Skip to content

Commit 57ad422

Browse files
Merge branch 'dev' into fix-is-outside-range-function
2 parents 067e587 + f6abdfb commit 57ad422

File tree

16 files changed

+140
-46
lines changed

16 files changed

+140
-46
lines changed

Diff for: .circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ jobs:
385385
PERCY_ENABLE: 1
386386
PERCY_PARALLEL_TOTAL: -1
387387

388-
parallelism: 4
388+
parallelism: 5
389389

390390
steps:
391391
- checkout:

Diff for: CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [2.3.1] - 2022-03-29
6+
7+
### Fixed
8+
9+
- [#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.
10+
- [#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'.
11+
512
## [2.3.0] - 2022-03-13
613

714
### Added

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Built on top of [Plotly.js](https://github.com/plotly/plotly.js), [React](https:
2323
|![Sample Dash App](https://user-images.githubusercontent.com/1280389/30086128-9bb4a28e-9267-11e7-8fe4-bbac7d53f2b0.gif) | Here’s a simple example of a Dash App that ties a Dropdown to a Plotly Graph. As the user selects a value in the Dropdown, the application code dynamically exports data from Google Finance into a Pandas DataFrame. This app was written in just **43** lines of code ([view the source](https://gist.github.com/chriddyp/3d2454905d8f01886d651f207e2419f0)). |
2424
|![Crossfiltering Dash App](https://user-images.githubusercontent.com/1280389/30086123-97c58bde-9267-11e7-98a0-7f626de5199a.gif)|Dash app code is declarative and reactive, which makes it easy to build complex apps that contain many interactive elements. Here’s an example with 5 inputs, 3 outputs, and cross filtering. This app was composed in just 160 lines of code, all of which were Python.|
2525
|![Dash App with Mapbox map showing walmart store openings](https://user-images.githubusercontent.com/1280389/30086299-768509d0-9268-11e7-8e6b-626ac9ca512c.gif)| Dash uses [Plotly.js](https://github.com/plotly/plotly.js) for charting. About 50 chart types are supported, including maps. |
26-
|![Financial report](https://github.com/plotly/dash-docs/blob/516f80c417051406210b94ea23a6d3b6cd84d146/assets/images/gallery/dash-financial-report.gif)| Dash isn't just for dashboards. You have full control over the look and feel of your applications. Here's a Dash App that's styled to look like a PDF report. |
26+
|![Financial report](https://user-images.githubusercontent.com/2678795/161153710-57952401-6e07-42d5-ba3e-bab6419998c7.gif)| Dash isn't just for dashboards. You have full control over the look and feel of your applications. Here's a Dash App that's styled to look like a PDF report. |
2727

2828
To learn more about Dash, read the [extensive announcement letter](https://medium.com/@plotlygraphs/introducing-dash-5ecf7191b503) or [jump in with the user guide](https://plotly.com/dash).
2929

@@ -53,4 +53,4 @@ Enterprise AI Features: Everything that your data science team needs to rapidly
5353

5454
See [https://plotly.com/contact-us/](https://plotly.com/contact-us/) to get in touch.
5555

56-
![image](https://images.prismic.io/plotly-marketing-website/493eec39-8467-4610-b9d0-d6ad3ea61423_Dash+Open+source%2BDash+enterprise2-01.jpg?auto=compress,format)
56+
![Dash Enterprise](https://user-images.githubusercontent.com/2678795/161155614-21c54a22-f821-4dda-b910-ee27e27fb5f2.png)

Diff for: components/dash-table/tests/selenium/test_sizing.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import dash
2-
import pytest
32

43
from utils import (
5-
basic_modes,
64
get_props,
7-
generate_mock_data,
8-
generate_markdown_mock_data,
9-
generate_mixed_markdown_data,
105
)
116

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

296291

297-
@pytest.mark.parametrize("props", basic_modes)
298-
@pytest.mark.parametrize(
299-
"data_fn",
300-
[generate_mock_data, generate_markdown_mock_data, generate_mixed_markdown_data],
301-
)
302-
def test_szng004_on_focus(test, props, data_fn):
292+
def on_focus(test, props, data_fn):
303293
app = dash.Dash(__name__)
304294

305295
baseProps1 = get_props(data_fn=data_fn)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
3+
from test_sizing import on_focus
4+
5+
from utils import (
6+
basic_modes,
7+
generate_mock_data,
8+
)
9+
10+
11+
@pytest.mark.parametrize("props", basic_modes)
12+
def test_szng004_on_focus(test, props):
13+
on_focus(test, props, generate_mock_data)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
3+
from test_sizing import on_focus
4+
5+
from utils import (
6+
basic_modes,
7+
generate_markdown_mock_data,
8+
)
9+
10+
11+
@pytest.mark.parametrize("props", basic_modes)
12+
def test_szng005_on_focus(test, props):
13+
on_focus(test, props, generate_markdown_mock_data)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
3+
from test_sizing import on_focus
4+
5+
from utils import (
6+
basic_modes,
7+
generate_mixed_markdown_data,
8+
)
9+
10+
11+
@pytest.mark.parametrize("props", basic_modes)
12+
def test_szng006_on_focus(test, props):
13+
on_focus(test, props, generate_mixed_markdown_data)

Diff for: dash/_dash_renderer.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.11.3"
1+
__version__ = "1.11.4"
22

33
_js_dist_dependencies = [
44
{
@@ -39,7 +39,7 @@
3939
{
4040
"relative_package_path": "dash-renderer/build/dash_renderer.min.js",
4141
"dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
42-
"external_url": "https://unpkg.com/[email protected].3"
42+
"external_url": "https://unpkg.com/[email protected].4"
4343
"/build/dash_renderer.min.js",
4444
"namespace": "dash",
4545
},

Diff for: dash/_utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import logging
99
import io
1010
import json
11+
import secrets
12+
import string
1113
from functools import wraps
1214

1315
logger = logging.getLogger()
@@ -206,3 +208,9 @@ def _wrapper(*args, **kwargs):
206208
return _wrapper
207209

208210
return wrapper
211+
212+
213+
def gen_salt(chars):
214+
return "".join(
215+
secrets.choice(string.ascii_letters + string.digits) for _ in range(chars)
216+
)

Diff for: dash/dash-renderer/package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: dash/dash-renderer/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-renderer",
3-
"version": "1.11.3",
3+
"version": "1.11.4",
44
"description": "render dash components in react",
55
"main": "build/dash_renderer.min.js",
66
"scripts": {

Diff for: dash/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function UnconnectedErrorContent({error, base}) {
110110
)}
111111
{/* Backend Error */}
112112
{typeof error.html !== 'string' ? null : error.html.indexOf(
113-
'<!DOCTYPE HTML'
113+
'<!DOCTYPE'
114114
) === 0 ? (
115115
<div className='dash-be-error__st'>
116116
<div className='dash-backend-error'>

Diff for: dash/dash.py

+44-9
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
import mimetypes
1212
import hashlib
1313
import base64
14+
import traceback
1415
from urllib.parse import urlparse
1516

1617
import flask
1718
from flask_compress import Compress
18-
from werkzeug.debug.tbtools import get_current_traceback
19+
1920
from pkg_resources import get_distribution, parse_version
2021
from dash import dcc
2122
from dash import html
@@ -48,6 +49,7 @@
4849
patch_collections_abc,
4950
split_callback_id,
5051
to_json,
52+
gen_salt,
5153
)
5254
from . import _callback
5355
from . import _get_paths
@@ -102,6 +104,42 @@
102104
_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer"
103105

104106

107+
def _get_traceback(secret, error: Exception):
108+
109+
try:
110+
# pylint: disable=import-outside-toplevel
111+
from werkzeug.debug import tbtools
112+
except ImportError:
113+
tbtools = None
114+
115+
def _get_skip(text, divider=2):
116+
skip = 0
117+
for i, line in enumerate(text):
118+
if "%% callback invoked %%" in line:
119+
skip = int((i + 1) / divider)
120+
break
121+
return skip
122+
123+
# werkzeug<2.1.0
124+
if hasattr(tbtools, "get_current_traceback"):
125+
tb = tbtools.get_current_traceback()
126+
skip = _get_skip(tb.plaintext.splitlines())
127+
return tbtools.get_current_traceback(skip=skip).render_full()
128+
129+
if hasattr(tbtools, "DebugTraceback"):
130+
tb = tbtools.DebugTraceback(error) # pylint: disable=no-member
131+
skip = _get_skip(tb.render_traceback_text().splitlines())
132+
133+
# pylint: disable=no-member
134+
return tbtools.DebugTraceback(error, skip=skip).render_debugger_html(
135+
True, secret, True
136+
)
137+
138+
tb = traceback.format_exception(type(error), error, error.__traceback__)
139+
skip = _get_skip(tb, 1)
140+
return tb[0] + "".join(tb[skip:])
141+
142+
105143
class _NoUpdate:
106144
# pylint: disable=too-few-public-methods
107145
pass
@@ -1756,19 +1794,16 @@ def enable_dev_tools(
17561794

17571795
if debug and dev_tools.prune_errors:
17581796

1797+
secret = gen_salt(20)
1798+
17591799
@self.server.errorhandler(Exception)
1760-
def _wrap_errors(_):
1800+
def _wrap_errors(error):
17611801
# find the callback invocation, if the error is from a callback
17621802
# and skip the traceback up to that point
17631803
# if the error didn't come from inside a callback, we won't
17641804
# skip anything.
1765-
tb = get_current_traceback()
1766-
skip = 0
1767-
for i, line in enumerate(tb.plaintext.splitlines()):
1768-
if "%% callback invoked %%" in line:
1769-
skip = int((i + 1) / 2)
1770-
break
1771-
return get_current_traceback(skip=skip).render_full(), 500
1805+
tb = _get_traceback(secret, error)
1806+
return tb, 500
17721807

17731808
if debug and dev_tools.ui:
17741809

Diff for: dash/testing/application_runners.py

+30-16
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import subprocess
88
import logging
99
import inspect
10+
import ctypes
1011

1112
import runpy
12-
import flask
1313
import requests
1414

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

104104

105+
class StoppableThread(threading.Thread):
106+
def get_id(self): # pylint: disable=R1710
107+
if hasattr(self, "_thread_id"):
108+
return self._thread_id
109+
for thread_id, thread in threading._active.items(): # pylint: disable=W0212
110+
if thread is self:
111+
return thread_id
112+
113+
def kill(self):
114+
thread_id = self.get_id()
115+
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
116+
ctypes.c_long(thread_id), ctypes.py_object(SystemExit)
117+
)
118+
if res == 0:
119+
raise ValueError(f"Invalid thread id: {thread_id}")
120+
if res > 1:
121+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None)
122+
raise SystemExit("Stopping thread failure")
123+
124+
105125
class ThreadedRunner(BaseDashRunner):
106126
"""Runs a dash application in a thread.
107127
@@ -110,25 +130,14 @@ class ThreadedRunner(BaseDashRunner):
110130

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

116-
@staticmethod
117-
def _stop_server():
118-
# https://werkzeug.palletsprojects.com/en/0.15.x/serving/#shutting-down-the-server
119-
stopper = flask.request.environ.get("werkzeug.server.shutdown")
120-
if stopper is None:
121-
raise RuntimeError("Not running with the Werkzeug Server")
122-
stopper()
123-
return "Flask server is shutting down"
124-
125135
# pylint: disable=arguments-differ
126136
def start(self, app, **kwargs):
127137
"""Start the app server in threading flavor."""
128-
app.server.add_url_rule(self.stop_route, self.stop_route, self._stop_server)
129138

130139
def _handle_error():
131-
self._stop_server()
140+
self.stop()
132141

133142
app.server.errorhandler(500)(_handle_error)
134143

@@ -139,9 +148,13 @@ def run():
139148
kwargs["port"] = self.port
140149
else:
141150
self.port = kwargs["port"]
142-
app.run_server(threaded=True, **kwargs)
143151

144-
self.thread = threading.Thread(target=run)
152+
try:
153+
app.run_server(threaded=True, **kwargs)
154+
except SystemExit:
155+
logger.info("Server stopped")
156+
157+
self.thread = StoppableThread(target=run)
145158
self.thread.daemon = True
146159
try:
147160
self.thread.start()
@@ -155,7 +168,8 @@ def run():
155168
wait.until(lambda: self.accessible(self.url), timeout=1)
156169

157170
def stop(self):
158-
requests.get("{}{}".format(self.url, self.stop_route))
171+
self.thread.kill()
172+
self.thread.join()
159173
wait.until_not(self.thread.is_alive, self.stop_timeout)
160174

161175

Diff for: dash/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.3.0"
1+
__version__ = "2.3.1"

Diff for: requires-ci.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Dependencies used by CI on github.com/plotly/dash
22
black==21.6b0
3+
click<8.1
34
dash-flow-example==0.0.5
45
dash-dangerously-set-inner-html
56
flake8==3.9.2

0 commit comments

Comments
 (0)