Skip to content

Commit 9d10871

Browse files
authored
Refactor JSON encoding to use plotly.py JSON engine (#1514)
* Use plotly.io.json.to_json_plotly for JSON serialization * Require plotly.py v5 * Run a few tests with both json and orjson encoders * CHANGELOG entry
1 parent e277be5 commit 9d10871

File tree

8 files changed

+81
-53
lines changed

8 files changed

+81
-53
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
66

77
## Dash and Dash Renderer
88

9+
### Added
10+
- [#1514](https://github.com/plotly/dash/pull/1514) Perform json encoding using the active plotly JSON engine. This will default to the faster orjson encoder if the `orjson` package is installed.
11+
12+
13+
914
### Changed
1015
- [#1707](https://github.com/plotly/dash/pull/1707) Change the default value of the `compress` argument to the `dash.Dash` constructor to `False`. This change reduces CPU usage, and was made in recognition of the fact that many deployment platforms (e.g. Dash Enterprise) already apply their own compression. If deploying to an environment that does not already provide compression, the Dash 1 behavior may be restored by adding `compress=True` to the `dash.Dash` constructor.
1116

dash/_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
_strings = (type(""), type(utils.bytes_to_native_str(b"")))
2121

2222

23+
def to_json(value):
24+
# pylint: disable=import-outside-toplevel
25+
from plotly.io.json import to_json_plotly
26+
27+
return to_json_plotly(value)
28+
29+
2330
def interpolate_str(template, **data):
2431
s = template
2532
for k, v in data.items():

dash/dash.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import sys
55
import collections
66
import importlib
7-
import json
87
import pkgutil
98
import threading
109
import re
@@ -22,8 +21,6 @@
2221
from werkzeug.debug.tbtools import get_current_traceback
2322
from pkg_resources import get_distribution, parse_version
2423

25-
import plotly
26-
2724
from .fingerprint import build_fingerprint, check_fingerprint
2825
from .resources import Scripts, Css
2926
from .dependencies import (
@@ -49,6 +46,7 @@
4946
split_callback_id,
5047
stringify_id,
5148
strip_relative_path,
49+
to_json,
5250
)
5351
from . import _dash_renderer
5452
from . import _validate
@@ -556,7 +554,7 @@ def serve_layout(self):
556554

557555
# TODO - Set browser cache limit - pass hash into frontend
558556
return flask.Response(
559-
json.dumps(layout, cls=plotly.utils.PlotlyJSONEncoder),
557+
to_json(layout),
560558
mimetype="application/json",
561559
)
562560

@@ -714,7 +712,7 @@ def _generate_scripts_html(self):
714712

715713
def _generate_config_html(self):
716714
return '<script id="_dash-config" type="application/json">{}</script>'.format(
717-
json.dumps(self._config(), cls=plotly.utils.PlotlyJSONEncoder)
715+
to_json(self._config())
718716
)
719717

720718
def _generate_renderer(self):
@@ -1109,9 +1107,7 @@ def add_context(*args, **kwargs):
11091107
response = {"response": component_ids, "multi": True}
11101108

11111109
try:
1112-
jsonResponse = json.dumps(
1113-
response, cls=plotly.utils.PlotlyJSONEncoder
1114-
)
1110+
jsonResponse = to_json(response)
11151111
except TypeError:
11161112
_validate.fail_callback_output(output_value, output)
11171113

requires-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ black==21.6b0
1010
fire==0.4.0
1111
coloredlogs==15.0.1
1212
flask-talisman==0.8.1
13+
orjson==3.3.1;python_version<"3.7"
14+
orjson==3.6.1;python_version>="3.7"

requires-install.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Flask>=1.0.4
22
flask-compress
3-
plotly
3+
plotly>=5.0.0
44
dash-core-components==1.17.1
55
dash-html-components==1.1.4
66
dash-table==4.12.0

tests/integration/callbacks/test_basic_callback.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from dash.dependencies import Input, Output, State
1717
from dash.exceptions import PreventUpdate
1818
from dash.testing import wait
19+
from tests.integration.utils import json_engine
1920

2021

2122
def test_cbsc001_simple_callback(dash_duo):
@@ -248,57 +249,61 @@ def update_out2(n_clicks, data):
248249
assert dash_duo.get_logs() == []
249250

250251

251-
def test_cbsc005_children_types(dash_duo):
252-
app = dash.Dash()
253-
app.layout = html.Div([html.Button(id="btn"), html.Div("init", id="out")])
254-
255-
outputs = [
256-
[None, ""],
257-
["a string", "a string"],
258-
[123, "123"],
259-
[123.45, "123.45"],
260-
[[6, 7, 8], "678"],
261-
[["a", "list", "of", "strings"], "alistofstrings"],
262-
[["strings", 2, "numbers"], "strings2numbers"],
263-
[["a string", html.Div("and a div")], "a string\nand a div"],
264-
]
265-
266-
@app.callback(Output("out", "children"), [Input("btn", "n_clicks")])
267-
def set_children(n):
268-
if n is None or n > len(outputs):
269-
return dash.no_update
270-
return outputs[n - 1][0]
252+
@pytest.mark.parametrize("engine", ["json", "orjson"])
253+
def test_cbsc005_children_types(dash_duo, engine):
254+
with json_engine(engine):
255+
app = dash.Dash()
256+
app.layout = html.Div([html.Button(id="btn"), html.Div("init", id="out")])
257+
258+
outputs = [
259+
[None, ""],
260+
["a string", "a string"],
261+
[123, "123"],
262+
[123.45, "123.45"],
263+
[[6, 7, 8], "678"],
264+
[["a", "list", "of", "strings"], "alistofstrings"],
265+
[["strings", 2, "numbers"], "strings2numbers"],
266+
[["a string", html.Div("and a div")], "a string\nand a div"],
267+
]
271268

272-
dash_duo.start_server(app)
273-
dash_duo.wait_for_text_to_equal("#out", "init")
269+
@app.callback(Output("out", "children"), [Input("btn", "n_clicks")])
270+
def set_children(n):
271+
if n is None or n > len(outputs):
272+
return dash.no_update
273+
return outputs[n - 1][0]
274274

275-
for children, text in outputs:
276-
dash_duo.find_element("#btn").click()
277-
dash_duo.wait_for_text_to_equal("#out", text)
275+
dash_duo.start_server(app)
276+
dash_duo.wait_for_text_to_equal("#out", "init")
278277

278+
for children, text in outputs:
279+
dash_duo.find_element("#btn").click()
280+
dash_duo.wait_for_text_to_equal("#out", text)
279281

280-
def test_cbsc006_array_of_objects(dash_duo):
281-
app = dash.Dash()
282-
app.layout = html.Div(
283-
[html.Button(id="btn"), dcc.Dropdown(id="dd"), html.Div(id="out")]
284-
)
285282

286-
@app.callback(Output("dd", "options"), [Input("btn", "n_clicks")])
287-
def set_options(n):
288-
return [{"label": "opt{}".format(i), "value": i} for i in range(n or 0)]
283+
@pytest.mark.parametrize("engine", ["json", "orjson"])
284+
def test_cbsc006_array_of_objects(dash_duo, engine):
285+
with json_engine(engine):
286+
app = dash.Dash()
287+
app.layout = html.Div(
288+
[html.Button(id="btn"), dcc.Dropdown(id="dd"), html.Div(id="out")]
289+
)
289290

290-
@app.callback(Output("out", "children"), [Input("dd", "options")])
291-
def set_out(opts):
292-
print(repr(opts))
293-
return len(opts)
291+
@app.callback(Output("dd", "options"), [Input("btn", "n_clicks")])
292+
def set_options(n):
293+
return [{"label": "opt{}".format(i), "value": i} for i in range(n or 0)]
294294

295-
dash_duo.start_server(app)
295+
@app.callback(Output("out", "children"), [Input("dd", "options")])
296+
def set_out(opts):
297+
print(repr(opts))
298+
return len(opts)
296299

297-
dash_duo.wait_for_text_to_equal("#out", "0")
298-
for i in range(5):
299-
dash_duo.find_element("#btn").click()
300-
dash_duo.wait_for_text_to_equal("#out", str(i + 1))
301-
dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i))
300+
dash_duo.start_server(app)
301+
302+
dash_duo.wait_for_text_to_equal("#out", "0")
303+
for i in range(5):
304+
dash_duo.find_element("#btn").click()
305+
dash_duo.wait_for_text_to_equal("#out", str(i + 1))
306+
dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i))
302307

303308

304309
@pytest.mark.parametrize("refresh", [False, True])

tests/integration/callbacks/test_malformed_request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def update_output(value):
3333
),
3434
)
3535
assert response.status_code == 200
36-
assert '"o1": {"children": 9}' in response.text
36+
assert '"o1":{"children":9}' in response.text
3737

3838
# now some bad ones
3939
outspecs = [

tests/integration/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import contextlib
23

34

45
TIMEOUT = 5 # Seconds
@@ -61,3 +62,15 @@ def wrapped_condition_function():
6162
time.sleep(0.5)
6263

6364
raise WaitForTimeout(get_message())
65+
66+
67+
@contextlib.contextmanager
68+
def json_engine(engine):
69+
import plotly.io as pio
70+
71+
original_engine = pio.json.config.default_engine
72+
try:
73+
pio.json.config.default_engine = engine
74+
yield
75+
finally:
76+
pio.json.config.default_engine = original_engine

0 commit comments

Comments
 (0)