From 73f57972af7981206a61e3e43b26f642e756ebb3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 9 Aug 2019 21:40:36 -0400 Subject: [PATCH 1/5] dev_tools_prune_errors --- dash/dash.py | 40 +++++++++-- .../devtools/test_devtools_error_handling.py | 72 ++++++++++++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 461bc7c4ab..d839f5b67a 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -19,6 +19,7 @@ import flask from flask import Flask, Response from flask_compress import Compress +from werkzeug.debug import get_current_traceback import plotly import dash_renderer @@ -1168,7 +1169,8 @@ def callback(self, output, inputs=[], state=[]): def wrap_func(func): @wraps(func) def add_context(*args, **kwargs): - output_value = func(*args, **kwargs) + # don't touch the comment on the next line - used by debugger + output_value = func(*args, **kwargs) # %% callback invoked %% if multi: if not isinstance(output_value, (list, tuple)): raise exceptions.InvalidCallbackReturnValue( @@ -1390,7 +1392,8 @@ def _setup_dev_tools(self, **kwargs): 'props_check', 'serve_dev_bundles', 'hot_reload', - 'silence_routes_logging' + 'silence_routes_logging', + 'prune_errors' ): dev_tools[attr] = get_combined_config( attr, kwargs.get(attr, None), default=debug @@ -1419,7 +1422,8 @@ def enable_dev_tools( dev_tools_hot_reload_interval=None, dev_tools_hot_reload_watch_interval=None, dev_tools_hot_reload_max_retry=None, - dev_tools_silence_routes_logging=None): + dev_tools_silence_routes_logging=None, + dev_tools_prune_errors=None): """ Activate the dev tools, called by `run_server`. If your application is served by wsgi and you want to activate the dev tools, you can call @@ -1483,6 +1487,11 @@ def enable_dev_tools( env: ``DASH_SILENCE_ROUTES_LOGGING`` :type dev_tools_silence_routes_logging: bool + :param dev_tools_prune_errors: Reduce tracebacks to just user code, + stripping out Flask and Dash pieces. `True` by default, set to + `False` to see the complete traceback. + :type dev_tools_prune_errors: bool + :return: debug """ if debug is None: @@ -1497,7 +1506,8 @@ def enable_dev_tools( hot_reload_interval=dev_tools_hot_reload_interval, hot_reload_watch_interval=dev_tools_hot_reload_watch_interval, hot_reload_max_retry=dev_tools_hot_reload_max_retry, - silence_routes_logging=dev_tools_silence_routes_logging + silence_routes_logging=dev_tools_silence_routes_logging, + prune_errors=dev_tools_prune_errors ) if dev_tools.silence_routes_logging: @@ -1527,6 +1537,21 @@ def enable_dev_tools( _reload.watch_thread.daemon = True _reload.watch_thread.start() + if debug and dev_tools.prune_errors: + @self.server.errorhandler(Exception) + def _wrap_errors(_): + # find the callback invocation, if the error is from a callback + # and skip the traceback up to that point + # if the error didn't come from inside a callback, we won't + # skip anything. + tb = get_current_traceback() + skip = 0 + for i, line in enumerate(tb.plaintext.splitlines()): + if "%% callback invoked %%" in line: + skip = int((i + 1) / 2) + break + return get_current_traceback(skip=skip).render_full(), 500 + if (debug and dev_tools.serve_dev_bundles and not self.scripts.config.serve_locally): # Dev bundles only works locally. @@ -1594,6 +1619,7 @@ def run_server( dev_tools_hot_reload_watch_interval=None, dev_tools_hot_reload_max_retry=None, dev_tools_silence_routes_logging=None, + dev_tools_prune_errors=None, **flask_run_options): """ Start the flask server in local mode, you should not run this on a @@ -1652,6 +1678,11 @@ def run_server( env: ``DASH_SILENCE_ROUTES_LOGGING`` :type dev_tools_silence_routes_logging: bool + :param dev_tools_prune_errors: Reduce tracebacks to just user code, + stripping out Flask and Dash pieces. Only available with debugging. + `True` by default, set to `False` to see the complete traceback. + :type dev_tools_prune_errors: bool + :param flask_run_options: Given to `Flask.run` :return: @@ -1666,6 +1697,7 @@ def run_server( dev_tools_hot_reload_watch_interval, dev_tools_hot_reload_max_retry, dev_tools_silence_routes_logging, + dev_tools_prune_errors ) if self._dev_tools.silence_routes_logging: diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index 398578cc76..c4e2f5c071 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -6,7 +6,7 @@ from dash.exceptions import PreventUpdate -def test_dveh001_python_errors(dash_duo): +def app_with_errors(): app = dash.Dash(__name__) app.layout = html.Div( @@ -19,10 +19,26 @@ def test_dveh001_python_errors(dash_duo): @app.callback(Output("output", "children"), [Input("python", "n_clicks")]) def update_output(n_clicks): if n_clicks == 1: - 1 / 0 + return bad_sub() elif n_clicks == 2: raise Exception("Special 2 clicks exception") + def bad_sub(): + return 1 / 0 + + return app + + +def get_error_html(dash_duo, index): + # error is in an iframe so is annoying to read out - get it from the store + return dash_duo.driver.execute_script( + "return store.getState().error.backEnd[{}].error.html;".format(index) + ) + + +def test_dveh001_python_errors(dash_duo): + app = app_with_errors() + dash_duo.start_server( app, debug=True, @@ -49,6 +65,58 @@ def update_output(n_clicks): dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - python exception - 2 errors open") + # the top (first) error is the most recent one - ie from the second click + error0 = get_error_html(dash_duo, 0) + # user part of the traceback shown by default + assert 'update_output' in error0 + assert 'Special 2 clicks exception' in error0 + assert 'bad_sub' not in error0 + # dash and flask part of the traceback not included + assert '%% callback invoked %%' not in error0 + assert 'self.wsgi_app' not in error0 + + error1 = get_error_html(dash_duo, 1) + assert 'update_output' in error1 + assert 'bad_sub' in error1 + assert 'ZeroDivisionError' in error1 + assert '%% callback invoked %%' not in error1 + assert 'self.wsgi_app' not in error1 + + +def test_dveh006_long_python_errors(dash_duo): + app = app_with_errors() + + dash_duo.start_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + dev_tools_prune_errors=False, + ) + + dash_duo.find_element("#python").click() + dash_duo.find_element("#python").click() + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") + + dash_duo.find_element(".test-devtools-error-toggle").click() + + error0 = get_error_html(dash_duo, 0) + assert 'update_output' in error0 + assert 'Special 2 clicks exception' in error0 + assert 'bad_sub' not in error0 + # dash and flask part of the traceback ARE included + # since we set dev_tools_prune_errors=False + assert '%% callback invoked %%' in error0 + assert 'self.wsgi_app' in error0 + + error1 = get_error_html(dash_duo, 1) + assert 'update_output' in error1 + assert 'bad_sub' in error1 + assert 'ZeroDivisionError' in error1 + assert '%% callback invoked %%' in error1 + assert 'self.wsgi_app' in error1 + def test_dveh002_prevent_update_not_in_error_msg(dash_duo): # raising PreventUpdate shouldn't display the error message From c250677a7ab6fc8a956797f5f187dc90576129ed Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 9 Aug 2019 22:08:50 -0400 Subject: [PATCH 2/5] changelog for dev_tools_prune_errors --- dash/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dash/CHANGELOG.md b/dash/CHANGELOG.md index 0596dcd312..edf9d7eb4a 100644 --- a/dash/CHANGELOG.md +++ b/dash/CHANGELOG.md @@ -1,3 +1,7 @@ +## [UNRELEASED] +### Added +- [#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. + ## [1.1.1] - 2019-08-06 ### Changed - 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) From d4f3e2ce1966decc74a3600ec194a3707cb79938 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 9 Aug 2019 22:13:50 -0400 Subject: [PATCH 3/5] more specific test of user function calls nearby context code is contained in the html --- .../devtools/test_devtools_error_handling.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index c4e2f5c071..b39ddf040e 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -68,16 +68,16 @@ def test_dveh001_python_errors(dash_duo): # the top (first) error is the most recent one - ie from the second click error0 = get_error_html(dash_duo, 0) # user part of the traceback shown by default - assert 'update_output' in error0 + assert 'in update_output' in error0 assert 'Special 2 clicks exception' in error0 - assert 'bad_sub' not in error0 + assert 'in bad_sub' not in error0 # dash and flask part of the traceback not included assert '%% callback invoked %%' not in error0 assert 'self.wsgi_app' not in error0 error1 = get_error_html(dash_duo, 1) - assert 'update_output' in error1 - assert 'bad_sub' in error1 + assert 'in update_output' in error1 + assert 'in bad_sub' in error1 assert 'ZeroDivisionError' in error1 assert '%% callback invoked %%' not in error1 assert 'self.wsgi_app' not in error1 @@ -102,17 +102,17 @@ def test_dveh006_long_python_errors(dash_duo): dash_duo.find_element(".test-devtools-error-toggle").click() error0 = get_error_html(dash_duo, 0) - assert 'update_output' in error0 + assert 'in update_output' in error0 assert 'Special 2 clicks exception' in error0 - assert 'bad_sub' not in error0 + assert 'in bad_sub' not in error0 # dash and flask part of the traceback ARE included # since we set dev_tools_prune_errors=False assert '%% callback invoked %%' in error0 assert 'self.wsgi_app' in error0 error1 = get_error_html(dash_duo, 1) - assert 'update_output' in error1 - assert 'bad_sub' in error1 + assert 'in update_output' in error1 + assert 'in bad_sub' in error1 assert 'ZeroDivisionError' in error1 assert '%% callback invoked %%' in error1 assert 'self.wsgi_app' in error1 From 3bab0179d72211b079e8f64730ecfd1e4ccf3cc0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 9 Aug 2019 22:58:21 -0400 Subject: [PATCH 4/5] update test_props_check for dcc.Checklist change percy auto-approve strikes again, I suppose --- tests/integration/devtools/test_props_check.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index 0f2fe200c8..931d973e35 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -15,7 +15,7 @@ "fail": True, "name": 'missing required "value" inside options', "component": dcc.Checklist, - "props": {"options": [{"label": "hello"}], "values": ["test"]}, + "props": {"options": [{"label": "hello"}], "value": ["test"]}, }, "invalid-nested-prop": { "fail": True, @@ -23,14 +23,14 @@ "component": dcc.Checklist, "props": { "options": [{"label": "hello", "value": True}], - "values": ["test"], + "value": ["test"], }, }, "invalid-arrayOf": { "fail": True, "name": "invalid arrayOf", "component": dcc.Checklist, - "props": {"options": "test", "values": []}, + "props": {"options": "test", "value": []}, }, "invalid-oneOf": { "fail": True, @@ -82,7 +82,7 @@ "component": dcc.Checklist, "props": { "options": [{"label": "hello", "value": "test"}], - "values": "test", + "value": "test", }, }, "no-properties": { From ee66038c8568ac0d5689693bc1d750b11cf8d6ba Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 10 Aug 2019 18:43:47 -0400 Subject: [PATCH 5/5] more direct get_current_traceback import Co-Authored-By: Byron Zhu --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index d839f5b67a..2d158d9e1b 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -19,7 +19,7 @@ import flask from flask import Flask, Response from flask_compress import Compress -from werkzeug.debug import get_current_traceback +from werkzeug.debug.tbtools import get_current_traceback import plotly import dash_renderer