Skip to content

Commit 98ef6b4

Browse files
get exceptions working on Py3.11+
previously exceptions were parsed line by line, however 3.11 introduced https://docs.python.org/3/whatsnew/3.11.html#whatsnew311-pep657 which means that exception line numbers and traceback skip offsets no longer line up. Rather than inspect the source code this change introduces an extra frame in `def _invoke_callback` that can be detected by walking the traceback and finding its code object.
1 parent 48a93fb commit 98ef6b4

File tree

3 files changed

+41
-27
lines changed

3 files changed

+41
-27
lines changed

dash/_callback.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
from ._callback_context import context_value
3636

3737

38+
def _invoke_callback(func, *args, **kwargs): # used to mark the frame for the debugger
39+
return func(*args, **kwargs) # %% callback invoked %%
40+
41+
3842
class NoUpdate:
3943
def to_plotly_json(self): # pylint: disable=no-self-use
4044
return {"_dash_no_update": "_dash_no_update"}
@@ -438,8 +442,7 @@ def add_context(*args, **kwargs):
438442
if output_value is callback_manager.UNDEFINED:
439443
return to_json(response)
440444
else:
441-
# don't touch the comment on the next line - used by debugger
442-
output_value = func(*func_args, **func_kwargs) # %% callback invoked %%
445+
output_value = _invoke_callback(func, *func_args, **func_kwargs)
443446

444447
if NoUpdate.is_no_update(output_value):
445448
raise PreventUpdate

dash/_jupyter.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import sys
99
import threading
1010
import time
11-
import traceback
1211

1312
from typing_extensions import Literal
1413

@@ -37,12 +36,16 @@
3736

3837

3938
def _get_skip(error: Exception):
40-
tb = traceback.format_exception(type(error), error, error.__traceback__)
41-
skip = 0
42-
for i, line in enumerate(tb):
43-
if "%% callback invoked %%" in line:
44-
skip = i + 1
45-
break
39+
from dash._callback import _invoke_callback # pylint: disable=import-outside-toplevel
40+
41+
tb = error.__traceback__
42+
skip = 1
43+
while tb.tb_next is not None:
44+
skip += 1
45+
tb = tb.tb_next
46+
if tb.tb_frame.f_code is _invoke_callback.__code__:
47+
return skip
48+
4649
return skip
4750

4851

dash/dash.py

+26-18
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,40 @@ def _get_traceback(secret, error: Exception):
146146
except ImportError:
147147
tbtools = None
148148

149-
def _get_skip(text, divider=2):
150-
skip = 0
151-
for i, line in enumerate(text):
152-
if "%% callback invoked %%" in line:
153-
skip = int((i + 1) / divider)
154-
break
149+
def _get_skip(error):
150+
from dash._callback import _invoke_callback # pylint: disable=import-outside-toplevel
151+
152+
tb = error.__traceback__
153+
skip = 1
154+
while tb.tb_next is not None:
155+
skip += 1
156+
tb = tb.tb_next
157+
if tb.tb_frame.f_code is _invoke_callback.__code__:
158+
return skip
159+
155160
return skip
156161

162+
def _do_skip(error):
163+
from dash._callback import _invoke_callback # pylint: disable=import-outside-toplevel
164+
165+
tb = error.__traceback__
166+
while tb.tb_next is not None:
167+
if tb.tb_frame.f_code is _invoke_callback.__code__:
168+
return tb.tb_next
169+
tb = tb.tb_next
170+
return error.__traceback__
171+
157172
# werkzeug<2.1.0
158173
if hasattr(tbtools, "get_current_traceback"):
159-
tb = tbtools.get_current_traceback()
160-
skip = _get_skip(tb.plaintext.splitlines())
161-
return tbtools.get_current_traceback(skip=skip).render_full()
174+
return tbtools.get_current_traceback(skip=_get_skip(error)).render_full()
162175

163176
if hasattr(tbtools, "DebugTraceback"):
164-
tb = tbtools.DebugTraceback(error) # pylint: disable=no-member
165-
skip = _get_skip(tb.render_traceback_text().splitlines())
166-
167177
# pylint: disable=no-member
168-
return tbtools.DebugTraceback(error, skip=skip).render_debugger_html(
169-
True, secret, True
170-
)
178+
return tbtools.DebugTraceback(
179+
error, skip=_get_skip(error)
180+
).render_debugger_html(True, secret, True)
171181

172-
tb = traceback.format_exception(type(error), error, error.__traceback__)
173-
skip = _get_skip(tb, 1)
174-
return tb[0] + "".join(tb[skip:])
182+
return "".join(traceback.format_exception(type(error), error, _do_skip(error)))
175183

176184

177185
# Singleton signal to not update an output, alternative to PreventUpdate

0 commit comments

Comments
 (0)