diff options
author | Erlend Egeberg Aasland <erlend.aasland@innova.no> | 2021-11-29 15:22:32 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-29 15:22:32 (GMT) |
commit | c4a69a4ad035513ada1c0d41a46723606b538e13 (patch) | |
tree | 29ef43642bc01b65bfa9305d3e3c74952eff2d01 | |
parent | 6ac3c8a3140c17bd71ba98dfc5250c371101e77c (diff) | |
download | cpython-c4a69a4ad035513ada1c0d41a46723606b538e13.zip cpython-c4a69a4ad035513ada1c0d41a46723606b538e13.tar.gz cpython-c4a69a4ad035513ada1c0d41a46723606b538e13.tar.bz2 |
bpo-45828: Use unraisable exceptions within sqlite3 callbacks (FH-29591)
-rw-r--r-- | Doc/library/sqlite3.rst | 24 | ||||
-rw-r--r-- | Doc/whatsnew/3.11.rst | 7 | ||||
-rw-r--r-- | Lib/test/test_sqlite3/test_hooks.py | 4 | ||||
-rw-r--r-- | Lib/test/test_sqlite3/test_userfunctions.py | 59 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2021-11-17-11-40-21.bpo-45828.kQU35U.rst | 2 | ||||
-rw-r--r-- | Modules/_sqlite/connection.c | 2 |
6 files changed, 64 insertions, 34 deletions
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 9fffe4d..fb38182 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -329,9 +329,27 @@ Module functions and constants By default you will not get any tracebacks in user-defined functions, aggregates, converters, authorizer callbacks etc. If you want to debug them, - you can call this function with *flag* set to ``True``. Afterwards, you will - get tracebacks from callbacks on ``sys.stderr``. Use :const:`False` to - disable the feature again. + you can call this function with *flag* set to :const:`True`. Afterwards, you + will get tracebacks from callbacks on :data:`sys.stderr`. Use :const:`False` + to disable the feature again. + + Register an :func:`unraisable hook handler <sys.unraisablehook>` for an + improved debug experience:: + + >>> import sqlite3 + >>> sqlite3.enable_callback_tracebacks(True) + >>> cx = sqlite3.connect(":memory:") + >>> cx.set_trace_callback(lambda stmt: 5/0) + >>> cx.execute("select 1") + Exception ignored in: <function <lambda> at 0x10b4e3ee0> + Traceback (most recent call last): + File "<stdin>", line 1, in <lambda> + ZeroDivisionError: division by zero + >>> import sys + >>> sys.unraisablehook = lambda unraisable: print(unraisable) + >>> cx.execute("select 1") + UnraisableHookArgs(exc_type=<class 'ZeroDivisionError'>, exc_value=ZeroDivisionError('division by zero'), exc_traceback=<traceback object at 0x10b559900>, err_msg=None, object=<function <lambda> at 0x10b4e3ee0>) + <sqlite3.Cursor object at 0x10b1fe840> .. _sqlite3-connection-objects: diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 9751f89..3b65921 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -248,7 +248,6 @@ sqlite3 (Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in :issue:`16379` and :issue:`24139`.) - * Add :meth:`~sqlite3.Connection.setlimit` and :meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for setting and getting SQLite limits by connection basis. @@ -258,6 +257,12 @@ sqlite3 threading mode the underlying SQLite library has been compiled with. (Contributed by Erlend E. Aasland in :issue:`45613`.) +* :mod:`sqlite3` C callbacks now use unraisable exceptions if callback + tracebacks are enabled. Users can now register an + :func:`unraisable hook handler <sys.unraisablehook>` to improve their debug + experience. + (Contributed by Erlend E. Aasland in :issue:`45828`.) + threading --------- diff --git a/Lib/test/test_sqlite3/test_hooks.py b/Lib/test/test_sqlite3/test_hooks.py index bf454b2..9e5e53a 100644 --- a/Lib/test/test_sqlite3/test_hooks.py +++ b/Lib/test/test_sqlite3/test_hooks.py @@ -197,7 +197,7 @@ class ProgressTests(unittest.TestCase): con.execute("select 1 union select 2 union select 3").fetchall() self.assertEqual(action, 0, "progress handler was not cleared") - @with_tracebacks(['bad_progress', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="bad_progress") def test_error_in_progress_handler(self): con = sqlite.connect(":memory:") def bad_progress(): @@ -208,7 +208,7 @@ class ProgressTests(unittest.TestCase): create table foo(a, b) """) - @with_tracebacks(['__bool__', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="bad_progress") def test_error_in_progress_handler_result(self): con = sqlite.connect(":memory:") class BadBool: diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 62a11a5..996437b 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -25,46 +25,52 @@ import contextlib import functools import gc import io +import re import sys import unittest import unittest.mock import sqlite3 as sqlite -from test.support import bigmemtest +from test.support import bigmemtest, catch_unraisable_exception from .test_dbapi import cx_limit -def with_tracebacks(strings, traceback=True): +def with_tracebacks(exc, regex="", name=""): """Convenience decorator for testing callback tracebacks.""" - if traceback: - strings.append('Traceback') - def decorator(func): + _regex = re.compile(regex) if regex else None @functools.wraps(func) def wrapper(self, *args, **kwargs): - # First, run the test with traceback enabled. - with check_tracebacks(self, strings): - func(self, *args, **kwargs) + with catch_unraisable_exception() as cm: + # First, run the test with traceback enabled. + with check_tracebacks(self, cm, exc, _regex, name): + func(self, *args, **kwargs) # Then run the test with traceback disabled. func(self, *args, **kwargs) return wrapper return decorator + @contextlib.contextmanager -def check_tracebacks(self, strings): +def check_tracebacks(self, cm, exc, regex, obj_name): """Convenience context manager for testing callback tracebacks.""" sqlite.enable_callback_tracebacks(True) try: buf = io.StringIO() with contextlib.redirect_stderr(buf): yield - tb = buf.getvalue() - for s in strings: - self.assertIn(s, tb) + + self.assertEqual(cm.unraisable.exc_type, exc) + if regex: + msg = str(cm.unraisable.exc_value) + self.assertIsNotNone(regex.search(msg)) + if obj_name: + self.assertEqual(cm.unraisable.object.__name__, obj_name) finally: sqlite.enable_callback_tracebacks(False) + def func_returntext(): return "foo" def func_returntextwithnull(): @@ -299,7 +305,7 @@ class FunctionTests(unittest.TestCase): val = cur.fetchone()[0] self.assertEqual(val, 1<<31) - @with_tracebacks(['func_raiseexception', '5/0', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="func_raiseexception") def test_func_exception(self): cur = self.con.cursor() with self.assertRaises(sqlite.OperationalError) as cm: @@ -307,14 +313,14 @@ class FunctionTests(unittest.TestCase): cur.fetchone() self.assertEqual(str(cm.exception), 'user-defined function raised exception') - @with_tracebacks(['func_memoryerror', 'MemoryError']) + @with_tracebacks(MemoryError, name="func_memoryerror") def test_func_memory_error(self): cur = self.con.cursor() with self.assertRaises(MemoryError): cur.execute("select memoryerror()") cur.fetchone() - @with_tracebacks(['func_overflowerror', 'OverflowError']) + @with_tracebacks(OverflowError, name="func_overflowerror") def test_func_overflow_error(self): cur = self.con.cursor() with self.assertRaises(sqlite.DataError): @@ -426,22 +432,21 @@ class FunctionTests(unittest.TestCase): del x,y gc.collect() + @with_tracebacks(OverflowError) def test_func_return_too_large_int(self): cur = self.con.cursor() for value in 2**63, -2**63-1, 2**64: self.con.create_function("largeint", 0, lambda value=value: value) - with check_tracebacks(self, ['OverflowError']): - with self.assertRaises(sqlite.DataError): - cur.execute("select largeint()") + with self.assertRaises(sqlite.DataError): + cur.execute("select largeint()") + @with_tracebacks(UnicodeEncodeError, "surrogates not allowed", "chr") def test_func_return_text_with_surrogates(self): cur = self.con.cursor() self.con.create_function("pychr", 1, chr) for value in 0xd8ff, 0xdcff: - with check_tracebacks(self, - ['UnicodeEncodeError', 'surrogates not allowed']): - with self.assertRaises(sqlite.OperationalError): - cur.execute("select pychr(?)", (value,)) + with self.assertRaises(sqlite.OperationalError): + cur.execute("select pychr(?)", (value,)) @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') @bigmemtest(size=2**31, memuse=3, dry_run=False) @@ -510,7 +515,7 @@ class AggregateTests(unittest.TestCase): val = cur.fetchone()[0] self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error") - @with_tracebacks(['__init__', '5/0', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInInit") def test_aggr_exception_in_init(self): cur = self.con.cursor() with self.assertRaises(sqlite.OperationalError) as cm: @@ -518,7 +523,7 @@ class AggregateTests(unittest.TestCase): val = cur.fetchone()[0] self.assertEqual(str(cm.exception), "user-defined aggregate's '__init__' method raised error") - @with_tracebacks(['step', '5/0', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInStep") def test_aggr_exception_in_step(self): cur = self.con.cursor() with self.assertRaises(sqlite.OperationalError) as cm: @@ -526,7 +531,7 @@ class AggregateTests(unittest.TestCase): val = cur.fetchone()[0] self.assertEqual(str(cm.exception), "user-defined aggregate's 'step' method raised error") - @with_tracebacks(['finalize', '5/0', 'ZeroDivisionError']) + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInFinalize") def test_aggr_exception_in_finalize(self): cur = self.con.cursor() with self.assertRaises(sqlite.OperationalError) as cm: @@ -643,11 +648,11 @@ class AuthorizerRaiseExceptionTests(AuthorizerTests): raise ValueError return sqlite.SQLITE_OK - @with_tracebacks(['authorizer_cb', 'ValueError']) + @with_tracebacks(ValueError, name="authorizer_cb") def test_table_access(self): super().test_table_access() - @with_tracebacks(['authorizer_cb', 'ValueError']) + @with_tracebacks(ValueError, name="authorizer_cb") def test_column_access(self): super().test_table_access() diff --git a/Misc/NEWS.d/next/Library/2021-11-17-11-40-21.bpo-45828.kQU35U.rst b/Misc/NEWS.d/next/Library/2021-11-17-11-40-21.bpo-45828.kQU35U.rst new file mode 100644 index 0000000..07ec273 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-11-17-11-40-21.bpo-45828.kQU35U.rst @@ -0,0 +1,2 @@ +:mod:`sqlite` C callbacks now use unraisable exceptions if callback +tracebacks are enabled. Patch by Erlend E. Aasland. diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 0bc9d1d..4f0baa6 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -691,7 +691,7 @@ print_or_clear_traceback(callback_context *ctx) assert(ctx != NULL); assert(ctx->state != NULL); if (ctx->state->enable_callback_tracebacks) { - PyErr_Print(); + PyErr_WriteUnraisable(ctx->callable); } else { PyErr_Clear(); |