]> granicus.if.org Git - python/commitdiff
bpo-18748: _pyio.IOBase emits unraisable exception (GH-13512)
authorVictor Stinner <vstinner@redhat.com>
Thu, 23 May 2019 01:45:09 +0000 (03:45 +0200)
committerGitHub <noreply@github.com>
Thu, 23 May 2019 01:45:09 +0000 (03:45 +0200)
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.

Doc/whatsnew/3.8.rst
Lib/_pyio.py
Lib/test/test_io.py

index 8c2b40de1142f5416e89ff2606107db58d1882b2..b91f7bca63c9f28652c084177100d11d40d3e14e 100644 (file)
@@ -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
 --
 
index af2ce30c27806234499c20d3df30e9b9db7f491d..be5e4266daffd8339cd5818d2f481ca4e5fef10d 100644 (file)
@@ -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 ###
 
index dc44e506b1d0f444c2d2e63f15a9df121f882cc9..2c3bf89066799703cd6324ab89e4b2be3d7aa228 100644 (file)
@@ -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