8000 bpo-45828: Use unraisable exceptions within sqlite3 callbacks (FH-29591) · python/cpython@c4a69a4 · GitHub
[go: up one dir, main page]

Skip to content

Commit c4a69a4

Browse files
author
Erlend Egeberg Aasland
authored
bpo-45828: Use unraisable exceptions within sqlite3 callbacks (FH-29591)
1 parent 6ac3c8a commit c4a69a4

File tree

6 files changed

+64
-34
lines changed

6 files changed

+64
-34
lines changed

Doc/library/sqlite3.rst

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,27 @@ Module functions and constants
329329

330330
By default you will not get any tracebacks in user-defined functions,
331331
aggregates, converters, authorizer callbacks etc. If you want to debug them,
332-
you can call this function with *flag* set to ``True``. Afterwards, you will
333-
get tracebacks from callbacks on ``sys.stderr``. Use :const:`False` to
334-
disable the feature again.
332+
you can call this function with *flag* set to :const:`True`. Afterwards, you
333+
will get tracebacks from callbacks on :data:`sys.stderr`. Use :const:`False`
334+
to disable the feature again.
335+
336+
Register an :func:`unraisable hook handler <sys.unraisablehook>` for an
337+
improved debug experience::
338+
339+
>>> import sqlite3
340+
>>> sqlite3.enable_callback_tracebacks(True)
341+
>>> cx = sqlite3.connect(":memory:")
342+
>>> cx.set_trace_callback(lambda stmt: 5/0)
343+
>>> cx.execute("select 1")
344+
Exception ignored in: <function <lambda> at 0x10b4e3ee0>
345+
Traceback (most recent call last):
346+
File "<stdin>", line 1, in <lambda>
347+
ZeroDivisionError: division by zero
348+
>>> import sys
349+
>>> sys.unraisablehook = lambda unraisable: print(unraisable)
350+
>>> cx.execute("select 1")
351+
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>)
352+
<sqlite3.Cursor object at 0x10b1fe840>
335353

336354

337355
.. _sqlite3-connection-objects:

Doc/whatsnew/3.11.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ sqlite3
248248
(Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in
249249
:issue:`16379` and :issue:`24139`.)
250250

251-
252251
* Add :meth:`~sqlite3.Connection.setlimit` and
253252
:meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for
254253
setting and getting SQLite limits by connection basis.
@@ -258,6 +257,12 @@ sqlite3
258257
threading mode the underlying SQLite library has been compiled with.
259258
(Contributed by Erlend E. Aasland in :issue:`45613`.)
260259

260+
* :mod:`sqlite3` C callbacks now use unraisable exceptions if callback
261+
tracebacks are enabled. Users can now register an
262+
:func:`unraisable hook handler <sys.unraisablehook>` to improve their debug
263+
experience.
264+
(Contributed by Erlend E. Aasland in :issue:`45828`.)
265+
261266

262267
threading
263268
---------

Lib/test/test_sqlite3/test_hooks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def progress():
197197
con.execute("select 1 union select 2 union select 3").fetchall()
198198
self.assertEqual(action, 0, "progress handler was not cleared")
199199

200-
@with_tracebacks(['bad_progress', 'ZeroDivisionError'])
200+
@with_tracebacks(ZeroDivisionError, name="bad_progress")
201201
def test_error_in_progress_handler(self):
202202
con = sqlite.connect(":memory:")
203203
def bad_progress():
@@ -208,7 +208,7 @@ def bad_progress():
208208
create table foo(a, b)
209209
""")
210210

211-
@with_tracebacks(['__bool__', 'ZeroDivisionError'])
211+
@with_tracebacks(ZeroDivisionError, name="bad_progress")
212212
def test_error_in_progress_handler_result(self):
213213
con = sqlite.connect(":memory:")
214214
class BadBool:

Lib/test/test_sqlite3/test_userfunctions.py

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,46 +25,52 @@
2525
import functools
2626
import gc
2727
import io
28+
import re
2829
import sys
2930
import unittest
3031
import unittest.mock
3132
import sqlite3 as sqlite
3233

33-
from test.support import bigmemtest
34+
from test.support import bigmemtest, catch_unraisable_exception
3435
from .test_dbapi import cx_limit
3536

3637

37-
def with_tracebacks(strings, traceback=True):
38+
def with_tracebacks(exc, regex="", name=""):
3839
"""Convenience decorator for testing callback tracebacks."""
39-
if traceback:
40-
strings.append('Traceback')
41-
4240
def decorator(func):
41+
_regex = re.compile(regex) if regex else None
4342
@functools.wraps(func)
4443
def wrapper(self, *args, **kwargs):
45-
# First, run the test with traceback enabled.
46-
with check_tracebacks(self, strings):
47-
func(self, *args, **kwargs)
44+
with catch_unraisable_exception() as cm:
45+
# First, run the test with traceback enabled.
46+
with check_tracebacks(self, cm, exc, _regex, name):
47+
func(self, *args, **kwargs)
4848

4949
# Then run the test with traceback disabled.
5050
func(self, *args, **kwargs)
5151
return wrapper
5252
return decorator
5353

54+
5455
@contextlib.contextmanager
55-
def check_tracebacks(self, strings):
56+
def check_tracebacks(self, cm, exc, regex, obj_name):
5657
"""Convenience context manager for testing callback tracebacks."""
5758
sqlite.enable_callback_tracebacks(True)
5859
try:
5960
buf = io.StringIO()
6061
with contextlib.redirect_stderr(buf):
6162
yield
62-
tb = buf.getvalue()
63-
for s in strings:
64-
self.assertIn(s, tb)
63+
64+
self.assertEqual(cm.unraisable.exc_type, exc)
65+
if regex:
66+
msg = str(cm.unraisable.exc_value)
67+
self.assertIsNotNone(regex.search(msg))
68+
if obj_name:
69+
self.assertEqual(cm.unraisable.object.__name__, obj_name)
6570
finally:
6671
sqlite.enable_callback_tracebacks(False)
6772

73+
6874
def func_returntext():
6975
return "foo"
7076
def func_returntextwithnull():
@@ -299,22 +305,22 @@ def test_func_return_long_long(self):
299305
val = cur.fetchone()[0]
300306
self.assertEqual(val, 1<<31)
301307

302-
@with_tracebacks(['func_raiseexception', '5/0', 'ZeroDivisionError'])
308+
@with_tracebacks(ZeroDivisionError, name="func_raiseexception")
303309
def test_func_exception(self):
304310
cur = self.con.cursor()
305311
with self.assertRaises(sqlite.OperationalError) as cm:
306312
cur.execute("select raiseexception()")
307313
cur.fetchone()
308314
self.assertEqual(str(cm.exception), 'user-defined function raised exception')
309315

310-
@with_tracebacks(['func_memoryerror', 'MemoryError'])
316+
@with_tracebacks(MemoryError, name="func_memoryerror")
311317
def test_func_memory_error(self):
312318
cur = self.con.cursor()
313319
with self.assertRaises(MemoryError):
314320
cur.execute("select memoryerror()")
315321
cur.fetchone()
316322

317-
@with_tracebacks(['func_overflowerror', 'OverflowError'])
323+
@with_tracebacks(OverflowError, name="func_overflowerror")
318324
def test_func_overflow_error(self):
319325
cur = self.con.cursor()
320326
with self.assertRaises(sqlite.DataError):
@@ -426,22 +432,21 @@ def md5sum(t):
426432
del x,y
427433
gc.collect()
428434

435+
@with_tracebacks(OverflowError)
429436
def test_func_return_too_large_int(self):
430437
cur = self.con.cursor()
431438
for value in 2**63, -2**63-1, 2**64:
432439
self.con.create_function("largeint", 0, lambda value=value: value)
433-
with check_tracebacks(self, ['OverflowError']):
434-
with self.assertRaises(sqlite.DataError):
435-
cur.execute("select largeint()")
440+
with self.assertRaises(sqlite.DataError):
441+
cur.execute("select largeint()")
436442

443+
@with_tracebacks(UnicodeEncodeError, "surrogates not allowed", "chr")
437444
def test_func_return_text_with_surrogates(self):
438445
cur = self.con.cursor()
439446
self.con.create_function("pychr", 1, chr)
440447
for value in 0xd8ff, 0xdcff:
441-
with check_tracebacks(self,
442-
['UnicodeEncodeError', 'surrogates not allowed']):
443-
with self.assertRaises(sqlite.OperationalError):
444-
cur.execute("select pychr(?)", (value,))
448+
with self.assertRaises(sqlite.OperationalError):
449+
cur.execute("select pychr(?)", (value,))
445450

446451
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
447452
@bigmemtest(size=2**31, memuse=3, dry_run=False)
@@ -510,23 +515,23 @@ def test_aggr_no_finalize(self):
510515
val = cur.fetchone()[0]
511516
self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error")
512517

513-
@with_tracebacks(['__init__', '5/0', 'ZeroDivisionError'])
518+
@with_tracebacks(ZeroDivisionError, name="AggrExceptionInInit")
514519
def test_aggr_exception_in_init(self):
515520
cur = self.con.cursor()
516521
with self.assertRaises(sqlite.OperationalError) as cm:
517522
cur.execute("select excInit(t) from test")
518523
val = cur.fetchone()[0]
519524
self.assertEqual(str(cm.exception), "user-defined aggregate's '__init__' method raised error")
520525

521-
@with_tracebacks(['step', '5/0', 'ZeroDivisionError'])
526+
@with_tracebacks(ZeroDivisionError, name="AggrExceptionInStep")
522527
def test_aggr_exception_in_step(self):
523528
cur = self.con.cursor()
524529
with self.assertRaises(sqlite.OperationalError) as cm:
525530
cur.execute("select excStep(t) from test")
526531
val = cur.fetchone()[0]
527532
self.assertEqual(str(cm.exception), "user-defined aggregate's 'step' method raised error")
528533

529-
@with_tracebacks(['finalize', '5/0', 'ZeroDivisionError'])
534+
@with_tracebacks(ZeroDivisionError, name="AggrExceptionInFinalize")
530535
def test_aggr_exception_in_finalize(self):
531536
cur = self.con.cursor()
532537
with self.assertRaises(sqlite.OperationalError) as cm:
@@ -643,11 +648,11 @@ def authorizer_cb(action, arg1, arg2, dbname, source):
643648
raise ValueError
644649
return sqlite.SQLITE_OK
645650

646-
@with_tracebacks(['authorizer_cb', 'ValueError'])
651+
@with_tracebacks(ValueError, name="authorizer_cb")
647652
def test_table_access(self):
648653
super().test_table_access()
649654

650-
@with_tracebacks(['authorizer_cb', 'ValueError'])
655+
@with_tracebacks(ValueError, name="authorizer_cb")
651656
def test_column_access(self):
652657
super().test_table_access()
653658

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`sqlite` C callbacks now use unraisable exceptions if callback
2+
tracebacks are enabled. Patch by Erlend E. Aasland.

Modules/_sqlite/connection.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ print_or_clear_traceback(callback_context *ctx)
691691
assert(ctx != NULL);
692692
assert(ctx->state != NULL);
693693
if (ctx->state->enable_callback_tracebacks) {
694-
PyErr_Print();
694+
PyErr_WriteUnraisable(ctx->callable);
695695
}
696696
else {
697697
PyErr_Clear();

0 commit comments

Comments
 (0)
0