Skip to content

Fix werkzeug 2.1.0 import & dev tools error html rendering. #1995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import logging
import io
import json
import secrets
import string
from functools import wraps

logger = logging.getLogger()
Expand Down Expand Up @@ -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)
)
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function UnconnectedErrorContent({error, base}) {
)}
{/* Backend Error */}
{typeof error.html !== 'string' ? null : error.html.indexOf(
'<!DOCTYPE HTML'
'<!DOCTYPE'
) === 0 ? (
<div className='dash-be-error__st'>
<div className='dash-backend-error'>
Expand Down
53 changes: 44 additions & 9 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +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.tbtools import get_current_traceback

from pkg_resources import get_distribution, parse_version
from dash import dcc
from dash import html
Expand Down Expand Up @@ -48,6 +49,7 @@
patch_collections_abc,
split_callback_id,
to_json,
gen_salt,
)
from . import _callback
from . import _get_paths
Expand Down Expand Up @@ -102,6 +104,42 @@
_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer"


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):
if "%% callback invoked %%" in line:
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.splitlines())
return tbtools.get_current_traceback(skip=skip).render_full()

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
)

tb = traceback.format_exception(type(error), error, error.__traceback__)
skip = _get_skip(tb, 1)
return tb[0] + "".join(tb[skip:])


class _NoUpdate:
# pylint: disable=too-few-public-methods
pass
Expand Down Expand Up @@ -1756,19 +1794,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:

Expand Down