Skip to content

Commit c17edc8

Browse files
authored
Merge pull request #860 from plotly/843-simple-errors
dev_tools_prune_errors
2 parents d5ff7fc + ee66038 commit c17edc8

File tree

4 files changed

+114
-10
lines changed

4 files changed

+114
-10
lines changed

dash/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [UNRELEASED]
2+
### Added
3+
- [#860](https://github.com/plotly/dash/pull/860) Adds a new arg `dev_tools_prune_errors` to `app.run_server` and `app.enable_dev_tools`. Default `True`, tracebacks only include user code and below. Set it to `False` for the previous behavior showing all the Dash and Flask parts of the stack.
4+
15
## [1.1.1] - 2019-08-06
26
### Changed
37
- Bumped dash-core-components version from 1.1.0 to [1.1.1](https://github.com/plotly/dash-core-components/blob/master/CHANGELOG.md#111---2019-08-06)

dash/dash.py

+36-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import flask
2020
from flask import Flask, Response
2121
from flask_compress import Compress
22+
from werkzeug.debug.tbtools import get_current_traceback
2223

2324
import plotly
2425
import dash_renderer
@@ -1168,7 +1169,8 @@ def callback(self, output, inputs=[], state=[]):
11681169
def wrap_func(func):
11691170
@wraps(func)
11701171
def add_context(*args, **kwargs):
1171-
output_value = func(*args, **kwargs)
1172+
# don't touch the comment on the next line - used by debugger
1173+
output_value = func(*args, **kwargs) # %% callback invoked %%
11721174
if multi:
11731175
if not isinstance(output_value, (list, tuple)):
11741176
raise exceptions.InvalidCallbackReturnValue(
@@ -1390,7 +1392,8 @@ def _setup_dev_tools(self, **kwargs):
13901392
'props_check',
13911393
'serve_dev_bundles',
13921394
'hot_reload',
1393-
'silence_routes_logging'
1395+
'silence_routes_logging',
1396+
'prune_errors'
13941397
):
13951398
dev_tools[attr] = get_combined_config(
13961399
attr, kwargs.get(attr, None), default=debug
@@ -1419,7 +1422,8 @@ def enable_dev_tools(
14191422
dev_tools_hot_reload_interval=None,
14201423
dev_tools_hot_reload_watch_interval=None,
14211424
dev_tools_hot_reload_max_retry=None,
1422-
dev_tools_silence_routes_logging=None):
1425+
dev_tools_silence_routes_logging=None,
1426+
dev_tools_prune_errors=None):
14231427
"""
14241428
Activate the dev tools, called by `run_server`. If your application is
14251429
served by wsgi and you want to activate the dev tools, you can call
@@ -1483,6 +1487,11 @@ def enable_dev_tools(
14831487
env: ``DASH_SILENCE_ROUTES_LOGGING``
14841488
:type dev_tools_silence_routes_logging: bool
14851489
1490+
:param dev_tools_prune_errors: Reduce tracebacks to just user code,
1491+
stripping out Flask and Dash pieces. `True` by default, set to
1492+
`False` to see the complete traceback.
1493+
:type dev_tools_prune_errors: bool
1494+
14861495
:return: debug
14871496
"""
14881497
if debug is None:
@@ -1497,7 +1506,8 @@ def enable_dev_tools(
14971506
hot_reload_interval=dev_tools_hot_reload_interval,
14981507
hot_reload_watch_interval=dev_tools_hot_reload_watch_interval,
14991508
hot_reload_max_retry=dev_tools_hot_reload_max_retry,
1500-
silence_routes_logging=dev_tools_silence_routes_logging
1509+
silence_routes_logging=dev_tools_silence_routes_logging,
1510+
prune_errors=dev_tools_prune_errors
15011511
)
15021512

15031513
if dev_tools.silence_routes_logging:
@@ -1527,6 +1537,21 @@ def enable_dev_tools(
15271537
_reload.watch_thread.daemon = True
15281538
_reload.watch_thread.start()
15291539

1540+
if debug and dev_tools.prune_errors:
1541+
@self.server.errorhandler(Exception)
1542+
def _wrap_errors(_):
1543+
# find the callback invocation, if the error is from a callback
1544+
# and skip the traceback up to that point
1545+
# if the error didn't come from inside a callback, we won't
1546+
# skip anything.
1547+
tb = get_current_traceback()
1548+
skip = 0
1549+
for i, line in enumerate(tb.plaintext.splitlines()):
1550+
if "%% callback invoked %%" in line:
1551+
skip = int((i + 1) / 2)
1552+
break
1553+
return get_current_traceback(skip=skip).render_full(), 500
1554+
15301555
if (debug and dev_tools.serve_dev_bundles and
15311556
not self.scripts.config.serve_locally):
15321557
# Dev bundles only works locally.
@@ -1594,6 +1619,7 @@ def run_server(
15941619
dev_tools_hot_reload_watch_interval=None,
15951620
dev_tools_hot_reload_max_retry=None,
15961621
dev_tools_silence_routes_logging=None,
1622+
dev_tools_prune_errors=None,
15971623
**flask_run_options):
15981624
"""
15991625
Start the flask server in local mode, you should not run this on a
@@ -1652,6 +1678,11 @@ def run_server(
16521678
env: ``DASH_SILENCE_ROUTES_LOGGING``
16531679
:type dev_tools_silence_routes_logging: bool
16541680
1681+
:param dev_tools_prune_errors: Reduce tracebacks to just user code,
1682+
stripping out Flask and Dash pieces. Only available with debugging.
1683+
`True` by default, set to `False` to see the complete traceback.
1684+
:type dev_tools_prune_errors: bool
1685+
16551686
:param flask_run_options: Given to `Flask.run`
16561687
16571688
:return:
@@ -1666,6 +1697,7 @@ def run_server(
16661697
dev_tools_hot_reload_watch_interval,
16671698
dev_tools_hot_reload_max_retry,
16681699
dev_tools_silence_routes_logging,
1700+
dev_tools_prune_errors
16691701
)
16701702

16711703
if self._dev_tools.silence_routes_logging:

tests/integration/devtools/test_devtools_error_handling.py

+70-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dash.exceptions import PreventUpdate
77

88

9-
def test_dveh001_python_errors(dash_duo):
9+
def app_with_errors():
1010
app = dash.Dash(__name__)
1111

1212
app.layout = html.Div(
@@ -19,10 +19,26 @@ def test_dveh001_python_errors(dash_duo):
1919
@app.callback(Output("output", "children"), [Input("python", "n_clicks")])
2020
def update_output(n_clicks):
2121
if n_clicks == 1:
22-
1 / 0
22+
return bad_sub()
2323
elif n_clicks == 2:
2424
raise Exception("Special 2 clicks exception")
2525

26+
def bad_sub():
27+
return 1 / 0
28+
29+
return app
30+
31+
32+
def get_error_html(dash_duo, index):
33+
# error is in an iframe so is annoying to read out - get it from the store
34+
return dash_duo.driver.execute_script(
35+
"return store.getState().error.backEnd[{}].error.html;".format(index)
36+
)
37+
38+
39+
def test_dveh001_python_errors(dash_duo):
40+
app = app_with_errors()
41+
2642
dash_duo.start_server(
2743
app,
2844
debug=True,
@@ -49,6 +65,58 @@ def update_output(n_clicks):
4965
dash_duo.find_element(".test-devtools-error-toggle").click()
5066
dash_duo.percy_snapshot("devtools - python exception - 2 errors open")
5167

68+
# the top (first) error is the most recent one - ie from the second click
69+
error0 = get_error_html(dash_duo, 0)
70+
# user part of the traceback shown by default
71+
assert 'in update_output' in error0
72+
assert 'Special 2 clicks exception' in error0
73+
assert 'in bad_sub' not in error0
74+
# dash and flask part of the traceback not included
75+
assert '%% callback invoked %%' not in error0
76+
assert 'self.wsgi_app' not in error0
77+
78+
error1 = get_error_html(dash_duo, 1)
79+
assert 'in update_output' in error1
80+
assert 'in bad_sub' in error1
81+
assert 'ZeroDivisionError' in error1
82+
assert '%% callback invoked %%' not in error1
83+
assert 'self.wsgi_app' not in error1
84+
85+
86+
def test_dveh006_long_python_errors(dash_duo):
87+
app = app_with_errors()
88+
89+
dash_duo.start_server(
90+
app,
91+
debug=True,
92+
use_reloader=False,
93+
use_debugger=True,
94+
dev_tools_hot_reload=False,
95+
dev_tools_prune_errors=False,
96+
)
97+
98+
dash_duo.find_element("#python").click()
99+
dash_duo.find_element("#python").click()
100+
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2")
101+
102+
dash_duo.find_element(".test-devtools-error-toggle").click()
103+
104+
error0 = get_error_html(dash_duo, 0)
105+
assert 'in update_output' in error0
106+
assert 'Special 2 clicks exception' in error0
107+
assert 'in bad_sub' not in error0
108+
# dash and flask part of the traceback ARE included
109+
# since we set dev_tools_prune_errors=False
110+
assert '%% callback invoked %%' in error0
111+
assert 'self.wsgi_app' in error0
112+
113+
error1 = get_error_html(dash_duo, 1)
114+
assert 'in update_output' in error1
115+
assert 'in bad_sub' in error1
116+
assert 'ZeroDivisionError' in error1
117+
assert '%% callback invoked %%' in error1
118+
assert 'self.wsgi_app' in error1
119+
52120

53121
def test_dveh002_prevent_update_not_in_error_msg(dash_duo):
54122
# raising PreventUpdate shouldn't display the error message

tests/integration/devtools/test_props_check.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
1515
"fail": True,
1616
"name": 'missing required "value" inside options',
1717
"component": dcc.Checklist,
18-
"props": {"options": [{"label": "hello"}], "values": ["test"]},
18+
"props": {"options": [{"label": "hello"}], "value": ["test"]},
1919
},
2020
"invalid-nested-prop": {
2121
"fail": True,
2222
"name": "invalid nested prop",
2323
"component": dcc.Checklist,
2424
"props": {
2525
"options": [{"label": "hello", "value": True}],
26-
"values": ["test"],
26+
"value": ["test"],
2727
},
2828
},
2929
"invalid-arrayOf": {
3030
"fail": True,
3131
"name": "invalid arrayOf",
3232
"component": dcc.Checklist,
33-
"props": {"options": "test", "values": []},
33+
"props": {"options": "test", "value": []},
3434
},
3535
"invalid-oneOf": {
3636
"fail": True,
@@ -82,7 +82,7 @@
8282
"component": dcc.Checklist,
8383
"props": {
8484
"options": [{"label": "hello", "value": "test"}],
85-
"values": "test",
85+
"value": "test",
8686
},
8787
},
8888
"no-properties": {

0 commit comments

Comments
 (0)