From 877aaa95d4d11890715aec1f982915dbbb72a606 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Mar 2022 12:17:40 -0400 Subject: [PATCH 1/3] Fix werkzeug 2.1.0 import & dev tools error html rendering. --- .../error/FrontEnd/FrontEndError.react.js | 2 +- dash/dash.py | 42 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/dash/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js b/dash/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js index 5703add4db..49939ea1de 100644 --- a/dash/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js +++ b/dash/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js @@ -110,7 +110,7 @@ function UnconnectedErrorContent({error, base}) { )} {/* Backend Error */} {typeof error.html !== 'string' ? null : error.html.indexOf( - '
diff --git a/dash/dash.py b/dash/dash.py index c0b6cb410d..963f1d829e 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -15,7 +15,10 @@ import flask from flask_compress import Compress -from werkzeug.debug.tbtools import get_current_traceback + +from werkzeug.debug import tbtools +from werkzeug.security import gen_salt + from pkg_resources import get_distribution, parse_version from dash import dcc from dash import html @@ -102,6 +105,30 @@ _re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer" +def _get_traceback(secret, error): + def _get_skip(text): + skip = 0 + for i, line in enumerate(text.splitlines()): + if "%% callback invoked %%" in line: + skip = int((i + 1) / 2) + break + return skip + + # werkzeug<2.1.0 + if hasattr(tbtools, "get_current_traceback"): + tb = tbtools.get_current_traceback() + skip = _get_skip(tb.plaintext) + return tbtools.get_current_traceback(skip=skip).render_full() + + tb = tbtools.DebugTraceback(error) # pylint: disable=no-member + skip = _get_skip(tb.render_traceback_text()) + + # pylint: disable=no-member + return tbtools.DebugTraceback(error, skip=skip).render_debugger_html( + True, secret, True + ) + + class _NoUpdate: # pylint: disable=too-few-public-methods pass @@ -1756,19 +1783,16 @@ def enable_dev_tools( if debug and dev_tools.prune_errors: + secret = gen_salt(20) + @self.server.errorhandler(Exception) - def _wrap_errors(_): + def _wrap_errors(error): # 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 + tb = _get_traceback(secret, error) + return tb, 500 if debug and dev_tools.ui: From 8c198b41b360e55c9dcee975ab7225731b1d5571 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Mar 2022 13:06:17 -0400 Subject: [PATCH 2/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b036f014cf..700fce7b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed - [#1963](https://github.com/plotly/dash/pull/1963) Fix [#1780](https://github.com/plotly/dash/issues/1780) flask shutdown deprecation warning when running dashduo threaded tests. +- [#1995](https://github.com/plotly/dash/pull/1995) Fix [#1992](https://github.com/plotly/dash/issues/1992) ImportError: cannot import name 'get_current_traceback' from 'werkzeug.debug.tbtools'. ## [2.3.0] - 2022-03-13 From 5e89d95225d17e4ae4b0fdd94fd1c05d7bfe8761 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 29 Mar 2022 15:26:15 -0400 Subject: [PATCH 3/3] Add fallback traceback formatting. --- dash/_utils.py | 8 ++++++++ dash/dash.py | 39 +++++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 31de7e88cb..83b2895554 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -8,6 +8,8 @@ import logging import io import json +import secrets +import string from functools import wraps logger = logging.getLogger() @@ -206,3 +208,9 @@ def _wrapper(*args, **kwargs): return _wrapper return wrapper + + +def gen_salt(chars): + return "".join( + secrets.choice(string.ascii_letters + string.digits) for _ in range(chars) + ) diff --git a/dash/dash.py b/dash/dash.py index 963f1d829e..cc04b48b10 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -11,14 +11,12 @@ import mimetypes import hashlib import base64 +import traceback from urllib.parse import urlparse import flask from flask_compress import Compress -from werkzeug.debug import tbtools -from werkzeug.security import gen_salt - from pkg_resources import get_distribution, parse_version from dash import dcc from dash import html @@ -51,6 +49,7 @@ patch_collections_abc, split_callback_id, to_json, + gen_salt, ) from . import _callback from . import _get_paths @@ -105,28 +104,40 @@ _re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer" -def _get_traceback(secret, error): - def _get_skip(text): +def _get_traceback(secret, error: Exception): + + try: + # pylint: disable=import-outside-toplevel + from werkzeug.debug import tbtools + except ImportError: + tbtools = None + + def _get_skip(text, divider=2): skip = 0 - for i, line in enumerate(text.splitlines()): + for i, line in enumerate(text): if "%% callback invoked %%" in line: - skip = int((i + 1) / 2) + skip = int((i + 1) / divider) break return skip # werkzeug<2.1.0 if hasattr(tbtools, "get_current_traceback"): tb = tbtools.get_current_traceback() - skip = _get_skip(tb.plaintext) + skip = _get_skip(tb.plaintext.splitlines()) return tbtools.get_current_traceback(skip=skip).render_full() - tb = tbtools.DebugTraceback(error) # pylint: disable=no-member - skip = _get_skip(tb.render_traceback_text()) + if hasattr(tbtools, "DebugTraceback"): + tb = tbtools.DebugTraceback(error) # pylint: disable=no-member + skip = _get_skip(tb.render_traceback_text().splitlines()) + + # pylint: disable=no-member + return tbtools.DebugTraceback(error, skip=skip).render_debugger_html( + True, secret, True + ) - # pylint: disable=no-member - return tbtools.DebugTraceback(error, skip=skip).render_debugger_html( - True, secret, True - ) + tb = traceback.format_exception(type(error), error, error.__traceback__) + skip = _get_skip(tb, 1) + return tb[0] + "".join(tb[skip:]) class _NoUpdate: