Skip to content

Commit

Permalink
Refactor JSON encoding to use plotly.py JSON engine (#1514)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jonmmease authored Aug 16, 2021
1 parent e277be5 commit 9d10871
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 53 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## Dash and Dash Renderer

### Added
- [#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.



### Changed
- [#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.

Expand Down
7 changes: 7 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
_strings = (type(""), type(utils.bytes_to_native_str(b"")))


def to_json(value):
# pylint: disable=import-outside-toplevel
from plotly.io.json import to_json_plotly

return to_json_plotly(value)


def interpolate_str(template, **data):
s = template
for k, v in data.items():
Expand Down
12 changes: 4 additions & 8 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import sys
import collections
import importlib
import json
import pkgutil
import threading
import re
Expand All @@ -22,8 +21,6 @@
from werkzeug.debug.tbtools import get_current_traceback
from pkg_resources import get_distribution, parse_version

import plotly

from .fingerprint import build_fingerprint, check_fingerprint
from .resources import Scripts, Css
from .dependencies import (
Expand All @@ -49,6 +46,7 @@
split_callback_id,
stringify_id,
strip_relative_path,
to_json,
)
from . import _dash_renderer
from . import _validate
Expand Down Expand Up @@ -556,7 +554,7 @@ def serve_layout(self):

# TODO - Set browser cache limit - pass hash into frontend
return flask.Response(
json.dumps(layout, cls=plotly.utils.PlotlyJSONEncoder),
to_json(layout),
mimetype="application/json",
)

Expand Down Expand Up @@ -714,7 +712,7 @@ def _generate_scripts_html(self):

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

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

try:
jsonResponse = json.dumps(
response, cls=plotly.utils.PlotlyJSONEncoder
)
jsonResponse = to_json(response)
except TypeError:
_validate.fail_callback_output(output_value, output)

Expand Down
2 changes: 2 additions & 0 deletions requires-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ black==21.6b0
fire==0.4.0
coloredlogs==15.0.1
flask-talisman==0.8.1
orjson==3.3.1;python_version<"3.7"
orjson==3.6.1;python_version>="3.7"
2 changes: 1 addition & 1 deletion requires-install.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Flask>=1.0.4
flask-compress
plotly
plotly>=5.0.0
dash-core-components==1.17.1
dash-html-components==1.1.4
dash-table==4.12.0
Expand Down
91 changes: 48 additions & 43 deletions tests/integration/callbacks/test_basic_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from dash.testing import wait
from tests.integration.utils import json_engine


def test_cbsc001_simple_callback(dash_duo):
Expand Down Expand Up @@ -248,57 +249,61 @@ def update_out2(n_clicks, data):
assert dash_duo.get_logs() == []


def test_cbsc005_children_types(dash_duo):
app = dash.Dash()
app.layout = html.Div([html.Button(id="btn"), html.Div("init", id="out")])

outputs = [
[None, ""],
["a string", "a string"],
[123, "123"],
[123.45, "123.45"],
[[6, 7, 8], "678"],
[["a", "list", "of", "strings"], "alistofstrings"],
[["strings", 2, "numbers"], "strings2numbers"],
[["a string", html.Div("and a div")], "a string\nand a div"],
]

@app.callback(Output("out", "children"), [Input("btn", "n_clicks")])
def set_children(n):
if n is None or n > len(outputs):
return dash.no_update
return outputs[n - 1][0]
@pytest.mark.parametrize("engine", ["json", "orjson"])
def test_cbsc005_children_types(dash_duo, engine):
with json_engine(engine):
app = dash.Dash()
app.layout = html.Div([html.Button(id="btn"), html.Div("init", id="out")])

outputs = [
[None, ""],
["a string", "a string"],
[123, "123"],
[123.45, "123.45"],
[[6, 7, 8], "678"],
[["a", "list", "of", "strings"], "alistofstrings"],
[["strings", 2, "numbers"], "strings2numbers"],
[["a string", html.Div("and a div")], "a string\nand a div"],
]

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

for children, text in outputs:
dash_duo.find_element("#btn").click()
dash_duo.wait_for_text_to_equal("#out", text)
dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#out", "init")

for children, text in outputs:
dash_duo.find_element("#btn").click()
dash_duo.wait_for_text_to_equal("#out", text)

def test_cbsc006_array_of_objects(dash_duo):
app = dash.Dash()
app.layout = html.Div(
[html.Button(id="btn"), dcc.Dropdown(id="dd"), html.Div(id="out")]
)

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

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

dash_duo.start_server(app)
@app.callback(Output("out", "children"), [Input("dd", "options")])
def set_out(opts):
print(repr(opts))
return len(opts)

dash_duo.wait_for_text_to_equal("#out", "0")
for i in range(5):
dash_duo.find_element("#btn").click()
dash_duo.wait_for_text_to_equal("#out", str(i + 1))
dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i))
dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#out", "0")
for i in range(5):
dash_duo.find_element("#btn").click()
dash_duo.wait_for_text_to_equal("#out", str(i + 1))
dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i))


@pytest.mark.parametrize("refresh", [False, True])
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/callbacks/test_malformed_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def update_output(value):
),
)
assert response.status_code == 200
assert '"o1": {"children": 9}' in response.text
assert '"o1":{"children":9}' in response.text

# now some bad ones
outspecs = [
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
import contextlib


TIMEOUT = 5 # Seconds
Expand Down Expand Up @@ -61,3 +62,15 @@ def wrapped_condition_function():
time.sleep(0.5)

raise WaitForTimeout(get_message())


@contextlib.contextmanager
def json_engine(engine):
import plotly.io as pio

original_engine = pio.json.config.default_engine
try:
pio.json.config.default_engine = engine
yield
finally:
pio.json.config.default_engine = original_engine

0 comments on commit 9d10871

Please sign in to comment.