55import flask .cli
66from threading import Thread
77from retrying import retry
8-
8+ import io
9+ import re
10+ import sys
911
1012from 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+
1119from .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 (
0 commit comments