Skip to content

Commit 560e3da

Browse files
delsimdelsim
andauthored
Extend to-json conversion to include lazy functions from Django (#408)
* Monkey-patch the dash json conversion to add in lazy evaluation of text for Django * Collect together patches * Fix location of module import from string Co-authored-by: delsim <dev@gibbsconsulting.ca>
1 parent 5f0aeba commit 560e3da

File tree

5 files changed

+155
-5
lines changed

5 files changed

+155
-5
lines changed

demo/demo/plotly_apps.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import pandas as pd
3333

3434
from django.core.cache import cache
35+
from django.utils.translation import gettext, gettext_lazy
3536

3637
import dash
3738
from dash import dcc, html
@@ -46,6 +47,7 @@
4647
from django_plotly_dash import DjangoDash
4748
from django_plotly_dash.consumers import send_to_pipe_channel
4849

50+
4951
#pylint: disable=too-many-arguments, unused-argument, unused-variable
5052

5153
app = DjangoDash('SimpleExample')
@@ -59,8 +61,8 @@
5961
html.Div(id='output-color'),
6062
dcc.RadioItems(
6163
id='dropdown-size',
62-
options=[{'label': i, 'value': j} for i, j in [('L', 'large'),
63-
('M', 'medium'),
64+
options=[{'label': i, 'value': j} for i, j in [('L', gettext('large')),
65+
('M', gettext_lazy('medium')),
6466
('S', 'small')]],
6567
value='medium'
6668
),

django_plotly_dash/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@
3333
# Monkeypatching
3434

3535
import django_plotly_dash._callback
36+
import django_plotly_dash._patches

django_plotly_dash/_patches.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
'''
2+
Collation of patches made to other python libraries, such as Dash itself.
3+
4+
If/when the patches are not needed they will be removed from this file.
5+
6+
7+
Copyright (c) 2022 Gibbs Consulting and others - see CONTRIBUTIONS.md
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in all
17+
copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
SOFTWARE.
26+
27+
'''
28+
29+
import json
30+
31+
32+
from plotly.io._json import config
33+
from plotly.utils import PlotlyJSONEncoder
34+
35+
from _plotly_utils.optional_imports import get_module
36+
from django.utils.encoding import force_text
37+
from django.utils.functional import Promise
38+
39+
40+
class DjangoPlotlyJSONEncoder(PlotlyJSONEncoder):
41+
"""Augment the PlotlyJSONEncoder class with Django delayed processing"""
42+
def default(self, obj):
43+
if isinstance(obj, Promise):
44+
return force_text(obj)
45+
return super().default(obj)
46+
47+
48+
def to_json_django_plotly(plotly_object, pretty=False, engine=None):
49+
"""
50+
Convert a plotly/Dash object to a JSON string representation
51+
52+
Parameters
53+
----------
54+
plotly_object:
55+
A plotly/Dash object represented as a dict, graph_object, or Dash component
56+
57+
pretty: bool (default False)
58+
True if JSON representation should be pretty-printed, False if
59+
representation should be as compact as possible.
60+
61+
engine: str (default None)
62+
The JSON encoding engine to use. One of:
63+
- "json" for an engine based on the built-in Python json module
64+
- "orjson" for a faster engine that requires the orjson package
65+
- "auto" for the "orjson" engine if available, otherwise "json"
66+
If not specified, the default engine is set to the current value of
67+
plotly.io.json.config.default_engine.
68+
69+
Returns
70+
-------
71+
str
72+
Representation of input object as a JSON string
73+
74+
See Also
75+
--------
76+
to_json : Convert a plotly Figure to JSON with validation
77+
"""
78+
orjson = get_module("orjson", should_load=True)
79+
80+
# Determine json engine
81+
if engine is None:
82+
engine = config.default_engine
83+
84+
if engine == "auto":
85+
if orjson is not None:
86+
engine = "orjson"
87+
else:
88+
engine = "json"
89+
elif engine not in ["orjson", "json"]:
90+
raise ValueError("Invalid json engine: %s" % engine)
91+
92+
modules = {
93+
"sage_all": get_module("sage.all", should_load=False),
94+
"np": get_module("numpy", should_load=False),
95+
"pd": get_module("pandas", should_load=False),
96+
"image": get_module("PIL.Image", should_load=False),
97+
}
98+
99+
# Dump to a JSON string and return
100+
# --------------------------------
101+
if engine == "json":
102+
opts = {}
103+
if pretty:
104+
opts["indent"] = 2
105+
else:
106+
# Remove all whitespace
107+
opts["separators"] = (",", ":")
108+
109+
return json.dumps(plotly_object, cls=DjangoPlotlyJSONEncoder, **opts)
110+
elif engine == "orjson":
111+
JsonConfig.validate_orjson()
112+
opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY
113+
114+
if pretty:
115+
opts |= orjson.OPT_INDENT_2
116+
117+
# Plotly
118+
try:
119+
plotly_object = plotly_object.to_plotly_json()
120+
except AttributeError:
121+
pass
122+
123+
# Try without cleaning
124+
try:
125+
return orjson.dumps(plotly_object, option=opts).decode("utf8")
126+
except TypeError:
127+
pass
128+
129+
cleaned = clean_to_json_compatible(
130+
plotly_object,
131+
numpy_allowed=True,
132+
datetime_allowed=True,
133+
modules=modules,
134+
)
135+
return orjson.dumps(cleaned, option=opts).decode("utf8")
136+
137+
138+
import plotly.io.json
139+
plotly.io.json.to_json_plotly = to_json_django_plotly

django_plotly_dash/dash_wrapper.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,12 @@
3636
from django.urls import reverse
3737
from django.utils.text import slugify
3838
from flask import Flask
39-
from plotly.utils import PlotlyJSONEncoder
4039

4140
from .app_name import app_name, main_view_label
4241
from .middleware import EmbeddedHolder
4342
from .util import serve_locally as serve_locally_setting
4443
from .util import stateless_app_lookup_hook
45-
from .util import static_asset_path
44+
from .util import static_asset_path, DjangoPlotlyJSONEncoder
4645

4746
try:
4847
from dataclasses import dataclass
@@ -480,7 +479,7 @@ def augment_initial_layout(self, base_response, initial_arguments=None):
480479
reworked_data = self.walk_tree_and_replace(baseData, overrides)
481480

482481
response_data = json.dumps(reworked_data,
483-
cls=PlotlyJSONEncoder)
482+
cls=DjangoPlotlyJSONEncoder)
484483

485484
return response_data, base_response.mimetype
486485

django_plotly_dash/util.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@
2525
import json
2626
import uuid
2727

28+
29+
from _plotly_utils.optional_imports import get_module
30+
31+
2832
from django.conf import settings
2933
from django.core.cache import cache
3034
from django.utils.module_loading import import_string
3135

36+
from django_plotly_dash._patches import DjangoPlotlyJSONEncoder
37+
38+
3239
def _get_settings():
3340
try:
3441
the_settings = settings.PLOTLY_DASH
@@ -133,3 +140,5 @@ def stateless_app_lookup_hook():
133140

134141
# Default is no additional lookup
135142
return lambda _: None
143+
144+

0 commit comments

Comments
 (0)