bpo-40795: ctypes calls unraisablehook with an exception (GH-20452)

If ctypes fails to convert the result of a callback or if a ctypes
callback function raises an exception, sys.unraisablehook is now
called with an exception set. Previously, the error was logged into
stderr by PyErr_Print().
This commit is contained in:
Victor Stinner 2020-05-28 00:38:12 +02:00 committed by GitHub
parent e80697d687
commit 10228bad04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 41 deletions

View file

@ -1,5 +1,7 @@
import functools
import unittest
from test import support
from ctypes import *
from ctypes.test import need_symbol
import _ctypes_test
@ -301,8 +303,22 @@ class SampleCallbacksTestCase(unittest.TestCase):
with self.assertRaises(ArgumentError):
cb(*args2)
def test_convert_result_error(self):
def func():
return ("tuple",)
proto = CFUNCTYPE(c_int)
ctypes_func = proto(func)
with support.catch_unraisable_exception() as cm:
# don't test the result since it is an uninitialized value
result = ctypes_func()
self.assertIsInstance(cm.unraisable.exc_value, TypeError)
self.assertEqual(cm.unraisable.err_msg,
"Exception ignored on converting result "
"of ctypes callback function")
self.assertIs(cm.unraisable.object, func)
################################################################
if __name__ == '__main__':
unittest.main()

View file

@ -1,5 +1,9 @@
from ctypes import *
import unittest, sys
import contextlib
from test import support
import unittest
import sys
def callback_func(arg):
42 / arg
@ -34,41 +38,40 @@ class CallbackTracbackTestCase(unittest.TestCase):
# created, then a full traceback printed. When SystemExit is
# raised in a callback function, the interpreter exits.
def capture_stderr(self, func, *args, **kw):
# helper - call function 'func', and return the captured stderr
import io
old_stderr = sys.stderr
logger = sys.stderr = io.StringIO()
try:
func(*args, **kw)
finally:
sys.stderr = old_stderr
return logger.getvalue()
@contextlib.contextmanager
def expect_unraisable(self, exc_type, exc_msg=None):
with support.catch_unraisable_exception() as cm:
yield
self.assertIsInstance(cm.unraisable.exc_value, exc_type)
if exc_msg is not None:
self.assertEqual(str(cm.unraisable.exc_value), exc_msg)
self.assertEqual(cm.unraisable.err_msg,
"Exception ignored on calling ctypes "
"callback function")
self.assertIs(cm.unraisable.object, callback_func)
def test_ValueError(self):
cb = CFUNCTYPE(c_int, c_int)(callback_func)
out = self.capture_stderr(cb, 42)
self.assertEqual(out.splitlines()[-1],
"ValueError: 42")
with self.expect_unraisable(ValueError, '42'):
cb(42)
def test_IntegerDivisionError(self):
cb = CFUNCTYPE(c_int, c_int)(callback_func)
out = self.capture_stderr(cb, 0)
self.assertEqual(out.splitlines()[-1][:19],
"ZeroDivisionError: ")
with self.expect_unraisable(ZeroDivisionError):
cb(0)
def test_FloatDivisionError(self):
cb = CFUNCTYPE(c_int, c_double)(callback_func)
out = self.capture_stderr(cb, 0.0)
self.assertEqual(out.splitlines()[-1][:19],
"ZeroDivisionError: ")
with self.expect_unraisable(ZeroDivisionError):
cb(0.0)
def test_TypeErrorDivisionError(self):
cb = CFUNCTYPE(c_int, c_char_p)(callback_func)
out = self.capture_stderr(cb, b"spam")
self.assertEqual(out.splitlines()[-1],
"TypeError: "
"unsupported operand type(s) for /: 'int' and 'bytes'")
err_msg = "unsupported operand type(s) for /: 'int' and 'bytes'"
with self.expect_unraisable(TypeError, err_msg):
cb(b"spam")
if __name__ == '__main__':
unittest.main()

View file

@ -27,7 +27,6 @@ for typ in [c_short, c_int, c_long, c_longlong,
class TestStructures(unittest.TestCase):
def test_native(self):
for typ in structures:
## print typ.value
self.assertEqual(typ.value.offset, 1)
o = typ()
o.value = 4
@ -35,7 +34,6 @@ class TestStructures(unittest.TestCase):
def test_swapped(self):
for typ in byteswapped_structures:
## print >> sys.stderr, typ.value
self.assertEqual(typ.value.offset, 1)
o = typ()
o.value = 4