Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit 3495a67

Browse files
committed
Add support for displaying tracebacks in front-end UI
Tracebacks use IPython logic to handle getting code from notebook cells
1 parent ea46355 commit 3495a67

File tree

2 files changed

+95
-20
lines changed

2 files changed

+95
-20
lines changed

jupyter_dash/jupyter_app.py

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55
import flask.cli
66
from threading import Thread
77
from retrying import retry
8-
8+
import io
9+
import re
10+
import sys
911

1012
from IPython.display import IFrame, display
13+
from IPython.core.ultratb import FormattedTB
14+
from ansi2html import Ansi2HTMLConverter
15+
16+
17+
from werkzeug.debug.tbtools import get_current_traceback
18+
1119
from .comms import _dash_comm, _jupyter_config, _request_jupyter_config
1220

1321

@@ -59,6 +67,8 @@ def __init__(self, name=None, server_url=None, **kwargs):
5967
except Exception:
6068
self._server_proxy = False
6169

70+
self._traceback = None
71+
6272
if ('base_subpath' in _jupyter_config and self._server_proxy and
6373
JupyterDash.default_requests_pathname_prefix is None):
6474
JupyterDash.default_requests_pathname_prefix = (
@@ -101,7 +111,7 @@ def alive():
101111

102112
def run_server(
103113
self,
104-
mode=None, width=800, height=650,
114+
mode=None, width=800, height=650, inline_exceptions=None,
105115
**kwargs
106116
):
107117
"""
@@ -119,6 +129,9 @@ def run_server(
119129
extension.
120130
:param width: Width of app when displayed using mode="inline"
121131
:param height: Height of app when displayed using mode="inline"
132+
:param inline_exceptions: If True, callback exceptions are displayed inline
133+
in the the notebook output cell. Defaults to True if mode=="inline",
134+
False otherwise.
122135
:param kwargs: Additional keyword arguments to pass to the superclass
123136
``Dash.run_server`` method.
124137
"""
@@ -151,6 +164,10 @@ def run_server(
151164
)
152165
)
153166

167+
# Infer inline_exceptions and ui
168+
if inline_exceptions is None:
169+
inline_exceptions = mode == "inline"
170+
154171
# Terminate any existing server using this port
155172
self._terminate_server_for_port(host, port)
156173

@@ -181,29 +198,43 @@ def run_server(
181198
server_url=server_url, requests_pathname_prefix=requests_pathname_prefix
182199
)
183200

184-
# Enable supported dev tools by default
185-
for k in [
186-
'dev_tools_silence_routes_logging',
187-
# 'dev_tools_ui', # Stack traces don't work yet
188-
'dev_tools_props_check',
189-
'dev_tools_serve_dev_bundles',
190-
'dev_tools_prune_errors'
191-
]:
192-
if k not in kwargs:
193-
kwargs[k] = True
194-
195-
if 'dev_tools_hot_reload' not in kwargs:
196-
# Enable hot-reload by default in "external" mode. Enabling in inline or
197-
# in JupyterLab extension seems to cause Jupyter problems sometimes when
198-
# there is no active kernel.
199-
kwargs['dev_tools_hot_reload'] = mode == "external"
200-
201-
# Disable debug because it doesn't work in notebook
201+
# Default the global "debug" flag to True
202+
debug = kwargs.get('debug', True)
203+
204+
# Disable debug flag when calling superclass because it doesn't work
205+
# in notebook
202206
kwargs['debug'] = False
203207

208+
# Enable supported dev tools
209+
if debug:
210+
for k in [
211+
'dev_tools_silence_routes_logging',
212+
'dev_tools_props_check',
213+
'dev_tools_serve_dev_bundles',
214+
'dev_tools_prune_errors'
215+
]:
216+
if k not in kwargs:
217+
kwargs[k] = True
218+
219+
# Enable dev tools by default unless app is displayed inline
220+
if 'dev_tools_ui' not in kwargs:
221+
kwargs['dev_tools_ui'] = mode != "inline"
222+
223+
if 'dev_tools_hot_reload' not in kwargs:
224+
# Enable hot-reload by default in "external" mode. Enabling in inline or
225+
# in JupyterLab extension seems to cause Jupyter problems sometimes when
226+
# there is no active kernel.
227+
kwargs['dev_tools_hot_reload'] = mode == "external"
228+
204229
# suppress warning banner printed to standard out
205230
flask.cli.show_server_banner = lambda *args, **kwargs: None
206231

232+
# Set up custom callback exception handling
233+
self._config_callback_exception_handling(
234+
dev_tools_prune_errors=kwargs.get('dev_tools_prune_errors', True),
235+
inline_exceptions=inline_exceptions,
236+
)
237+
207238
@retry(
208239
stop_max_attempt_number=15,
209240
wait_exponential_multiplier=100,
@@ -258,6 +289,49 @@ def wait_for_app():
258289
'url': dashboard_url,
259290
})
260291

292+
def _config_callback_exception_handling(
293+
self, dev_tools_prune_errors, inline_exceptions
294+
):
295+
296+
@self.server.errorhandler(Exception)
297+
def _wrap_errors(_):
298+
"""Install traceback handling for callbacks"""
299+
self._traceback = sys.exc_info()[2]
300+
301+
# Compute number of stack frames to skip to get down to callback
302+
tb = get_current_traceback()
303+
skip = 0
304+
if dev_tools_prune_errors:
305+
for i, line in enumerate(tb.plaintext.splitlines()):
306+
if "%% callback invoked %%" in line:
307+
skip = int((i + 1) / 2)
308+
break
309+
310+
# Use IPython traceback formatting to build colored ANSI traceback string
311+
ostream = io.StringIO()
312+
ipytb = FormattedTB(
313+
tb_offset=skip,
314+
mode="Verbose",
315+
color_scheme="Linux",
316+
include_vars=True,
317+
ostream=ostream
318+
)
319+
ipytb()
320+
321+
# Print colored ANSI representation if requested
322+
if inline_exceptions:
323+
print(ostream.getvalue())
324+
325+
# Use ansi2html to convert the colored ANSI string to HTML
326+
conv = Ansi2HTMLConverter(scheme="ansi2html", dark_bg=False)
327+
html_str = conv.convert(ostream.getvalue())
328+
329+
# Remove explicit background color so Dash dev-tools can set background
330+
# color
331+
html_str = re.sub("background-color:[^;]+;", "", html_str)
332+
333+
return html_str, 500
334+
261335
@classmethod
262336
def _terminate_server_for_port(cls, host, port):
263337
shutdown_url = "http://{host}:{port}/_shutdown".format(

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ flask
44
retrying
55
ipython
66
ipykernel
7+
ansi2html

0 commit comments

Comments
 (0)