summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/whatsnew/3.8.rst9
-rw-r--r--Lib/_pyio.py23
-rw-r--r--Lib/test/test_io.py64
3 files changed, 51 insertions, 45 deletions
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index 8c2b40d..b91f7bc 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -318,6 +318,15 @@ for :func:`property`, :func:`classmethod`, and :func:`staticmethod`::
self.bit_rate = round(bit_rate / 1000.0, 1)
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
--
diff --git a/Lib/_pyio.py b/Lib/_pyio.py
index af2ce30..be5e426 100644
--- a/Lib/_pyio.py
+++ b/Lib/_pyio.py
@@ -33,6 +33,10 @@ DEFAULT_BUFFER_SIZE = 8 * 1024 # bytes
# Rebind for compatibility
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,
newline=None, closefd=True, opener=None):
@@ -378,15 +382,18 @@ class IOBase(metaclass=abc.ABCMeta):
def __del__(self):
"""Destructor. Calls 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:
+ if _IOBASE_EMITS_UNRAISABLE:
self.close()
- except:
- pass
+ else:
+ # 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 ###
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index dc44e50..2c3bf89 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -67,9 +67,9 @@ MEMORY_SANITIZER = (
'--with-memory-sanitizer' in _config_args
)
-# Does io.IOBase logs unhandled exceptions on calling close()?
-# They are silenced by default in release build.
-DESTRUCTOR_LOG_ERRORS = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
+# 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 _default_chunk_size():
@@ -1098,23 +1098,18 @@ class CommonBufferedTests:
# Test that the exception state is not modified by a destructor,
# even if close() fails.
rawio = self.CloseFailureIO()
- def f():
- self.tp(rawio).xyzzy
- with support.captured_output("stderr") as s:
- self.assertRaises(AttributeError, f)
- s = s.getvalue().strip()
- if s:
- # The destructor *may* have printed an unraisable error, check it
- lines = s.splitlines()
- if DESTRUCTOR_LOG_ERRORS:
- self.assertEqual(len(lines), 5)
- 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)
+ try:
+ with support.catch_unraisable_exception() as cm:
+ with self.assertRaises(AttributeError):
+ self.tp(rawio).xyzzy
+
+ if not IOBASE_EMITS_UNRAISABLE:
+ self.assertIsNone(cm.unraisable)
+ elif cm.unraisable is not None:
+ self.assertEqual(cm.unraisable.exc_type, OSError)
+ finally:
+ # Explicitly break reference cycle
+ cm = None
def test_repr(self):
raw = self.MockRawIO()
@@ -2859,23 +2854,18 @@ class TextIOWrapperTest(unittest.TestCase):
# Test that the exception state is not modified by a destructor,
# even if close() fails.
rawio = self.CloseFailureIO()
- def f():
- self.TextIOWrapper(rawio).xyzzy
- with support.captured_output("stderr") as s:
- self.assertRaises(AttributeError, f)
- s = s.getvalue().strip()
- if s:
- # The destructor *may* have printed an unraisable error, check it
- lines = s.splitlines()
- if DESTRUCTOR_LOG_ERRORS:
- self.assertEqual(len(lines), 5)
- 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)
+ try:
+ with support.catch_unraisable_exception() as cm:
+ with self.assertRaises(AttributeError):
+ self.TextIOWrapper(rawio).xyzzy
+
+ if not IOBASE_EMITS_UNRAISABLE:
+ self.assertIsNone(cm.unraisable)
+ elif cm.unraisable is not None:
+ self.assertEqual(cm.unraisable.exc_type, OSError)
+ finally:
+ # Explicitly break reference cycle
+ cm = None
# Systematic tests of the text I/O API