Skip to content

Commit a768595

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 6c0bd06 commit a768595

File tree

3 files changed

+47
-27
lines changed

3 files changed

+47
-27
lines changed

Diff for: 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

Diff for: dash/_jupyter.py

+12-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,18 @@
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 ( # pylint: disable=import-outside-toplevel
40+
_invoke_callback,
41+
)
42+
43+
tb = error.__traceback__
44+
skip = 1
45+
while tb.tb_next is not None:
46+
skip += 1
47+
tb = tb.tb_next
48+
if tb.tb_frame.f_code is _invoke_callback.__code__:
49+
return skip
50+
4651
return skip
4752

4853

Diff for: dash/dash.py

+30-18
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,44 @@ 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 ( # pylint: disable=import-outside-toplevel
151+
_invoke_callback,
152+
)
153+
154+
tb = error.__traceback__
155+
skip = 1
156+
while tb.tb_next is not None:
157+
skip += 1
158+
tb = tb.tb_next
159+
if tb.tb_frame.f_code is _invoke_callback.__code__:
160+
return skip
161+
155162
return skip
156163

164+
def _do_skip(error):
165+
from dash._callback import ( # pylint: disable=import-outside-toplevel
166+
_invoke_callback,
167+
)
168+
169+
tb = error.__traceback__
170+
while tb.tb_next is not None:
171+
if tb.tb_frame.f_code is _invoke_callback.__code__:
172+
return tb.tb_next
173+
tb = tb.tb_next
174+
return error.__traceback__
175+
157176
# werkzeug<2.1.0
158177
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()
178+
return tbtools.get_current_traceback(skip=_get_skip(error)).render_full()
162179

163180
if hasattr(tbtools, "DebugTraceback"):
164-
tb = tbtools.DebugTraceback(error) # pylint: disable=no-member
165-
skip = _get_skip(tb.render_traceback_text().splitlines())
166-
167181
# pylint: disable=no-member
168-
return tbtools.DebugTraceback(error, skip=skip).render_debugger_html(
169-
True, secret, True
170-
)
182+
return tbtools.DebugTraceback(
183+
error, skip=_get_skip(error)
184+
).render_debugger_html(True, secret, True)
171185

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

176188

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

0 commit comments

Comments
 (0)