Skip to content

Commit

Permalink
Merge branch 'dev' into master-2.3.1
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcjohnson committed Mar 29, 2022
2 parents 8ec69cc + 1ab5aba commit f495cf3
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ jobs:
PERCY_ENABLE: 1
PERCY_PARALLEL_TOTAL: -1

parallelism: 4
parallelism: 5

steps:
- checkout:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [2.3.1] - 2022-03-29

### 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

### Added
Expand Down
12 changes: 1 addition & 11 deletions components/dash-table/tests/selenium/test_sizing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import dash
import pytest

from utils import (
basic_modes,
get_props,
generate_mock_data,
generate_markdown_mock_data,
generate_mixed_markdown_data,
)

from dash.dependencies import Input, Output
Expand Down Expand Up @@ -294,12 +289,7 @@ def test_szng002_percentages_result_in_same_widths(test):
assert test.get_log_errors() == []


@pytest.mark.parametrize("props", basic_modes)
@pytest.mark.parametrize(
"data_fn",
[generate_mock_data, generate_markdown_mock_data, generate_mixed_markdown_data],
)
def test_szng004_on_focus(test, props, data_fn):
def on_focus(test, props, data_fn):
app = dash.Dash(__name__)

baseProps1 = get_props(data_fn=data_fn)
Expand Down
13 changes: 13 additions & 0 deletions components/dash-table/tests/selenium/test_sizing_x.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest

from test_sizing import on_focus

from utils import (
basic_modes,
generate_mock_data,
)


@pytest.mark.parametrize("props", basic_modes)
def test_szng004_on_focus(test, props):
on_focus(test, props, generate_mock_data)
13 changes: 13 additions & 0 deletions components/dash-table/tests/selenium/test_sizing_y.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest

from test_sizing import on_focus

from utils import (
basic_modes,
generate_markdown_mock_data,
)


@pytest.mark.parametrize("props", basic_modes)
def test_szng005_on_focus(test, props):
on_focus(test, props, generate_markdown_mock_data)
13 changes: 13 additions & 0 deletions components/dash-table/tests/selenium/test_sizing_z.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest

from test_sizing import on_focus

from utils import (
basic_modes,
generate_mixed_markdown_data,
)


@pytest.mark.parametrize("props", basic_modes)
def test_szng006_on_focus(test, props):
on_focus(test, props, generate_mixed_markdown_data)
4 changes: 2 additions & 2 deletions dash/_dash_renderer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.11.3"
__version__ = "1.11.4"

_js_dist_dependencies = [
{
Expand Down Expand Up @@ -39,7 +39,7 @@
{
"relative_package_path": "dash-renderer/build/dash_renderer.min.js",
"dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
"external_url": "https://unpkg.com/[email protected].3"
"external_url": "https://unpkg.com/[email protected].4"
"/build/dash_renderer.min.js",
"namespace": "dash",
},
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)
)
4 changes: 2 additions & 2 deletions dash/dash-renderer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dash/dash-renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-renderer",
"version": "1.11.3",
"version": "1.11.4",
"description": "render dash components in react",
"main": "build/dash_renderer.min.js",
"scripts": {
Expand Down
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
46 changes: 30 additions & 16 deletions dash/testing/application_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import subprocess
import logging
import inspect
import ctypes

import runpy
import flask
import requests

from dash.testing.errors import NoAppFoundError, TestingTimeoutError, ServerCloseError
Expand Down Expand Up @@ -102,6 +102,26 @@ def tmp_app_path(self):
return self._tmp_app_path


class StoppableThread(threading.Thread):
def get_id(self): # pylint: disable=R1710
if hasattr(self, "_thread_id"):
return self._thread_id
for thread_id, thread in threading._active.items(): # pylint: disable=W0212
if thread is self:
return thread_id

def kill(self):
thread_id = self.get_id()
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_long(thread_id), ctypes.py_object(SystemExit)
)
if res == 0:
raise ValueError(f"Invalid thread id: {thread_id}")
if res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None)
raise SystemExit("Stopping thread failure")


class ThreadedRunner(BaseDashRunner):
"""Runs a dash application in a thread.
Expand All @@ -110,25 +130,14 @@ class ThreadedRunner(BaseDashRunner):

def __init__(self, keep_open=False, stop_timeout=3):
super().__init__(keep_open=keep_open, stop_timeout=stop_timeout)
self.stop_route = "/_stop-{}".format(uuid.uuid4().hex)
self.thread = None

@staticmethod
def _stop_server():
# https://werkzeug.palletsprojects.com/en/0.15.x/serving/#shutting-down-the-server
stopper = flask.request.environ.get("werkzeug.server.shutdown")
if stopper is None:
raise RuntimeError("Not running with the Werkzeug Server")
stopper()
return "Flask server is shutting down"

# pylint: disable=arguments-differ
def start(self, app, **kwargs):
"""Start the app server in threading flavor."""
app.server.add_url_rule(self.stop_route, self.stop_route, self._stop_server)

def _handle_error():
self._stop_server()
self.stop()

app.server.errorhandler(500)(_handle_error)

Expand All @@ -139,9 +148,13 @@ def run():
kwargs["port"] = self.port
else:
self.port = kwargs["port"]
app.run_server(threaded=True, **kwargs)

self.thread = threading.Thread(target=run)
try:
app.run_server(threaded=True, **kwargs)
except SystemExit:
logger.info("Server stopped")

self.thread = StoppableThread(target=run)
self.thread.daemon = True
try:
self.thread.start()
Expand All @@ -155,7 +168,8 @@ def run():
wait.until(lambda: self.accessible(self.url), timeout=1)

def stop(self):
requests.get("{}{}".format(self.url, self.stop_route))
self.thread.kill()
self.thread.join()
wait.until_not(self.thread.is_alive, self.stop_timeout)


Expand Down
2 changes: 1 addition & 1 deletion dash/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.3.0"
__version__ = "2.3.1"
1 change: 1 addition & 0 deletions requires-ci.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dependencies used by CI on github.com/plotly/dash
black==21.6b0
click<8.1
dash-flow-example==0.0.5
dash-dangerously-set-inner-html
flake8==3.9.2
Expand Down

0 comments on commit f495cf3

Please sign in to comment.