Skip to content

Commit 240f62c

Browse files
parse JSON response to update DA state only if needed (#324)
* parse JSON response to update DA state only if needed - update test to cover updating state via output - add lazy parsing of json response * add comment and simplify LazyJson * move import of typing outside try:except: block related to dataclasses Co-authored-by: GFJ138 <[email protected]>
1 parent 5ef2d7c commit 240f62c

File tree

4 files changed

+326
-291
lines changed

4 files changed

+326
-291
lines changed

django_plotly_dash/dash_wrapper.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,28 @@
2424
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2525
SOFTWARE.
2626
'''
27+
import inspect
2728
import itertools
2829
import json
29-
import inspect
3030
import warnings
31+
from typing import Dict, List, Callable
3132

3233
import dash
3334
from dash import Dash, dependencies
3435
from dash._utils import split_callback_id, inputs_to_dict
35-
36-
from flask import Flask
37-
3836
from django.urls import reverse
3937
from django.utils.text import slugify
40-
38+
from flask import Flask
4139
from plotly.utils import PlotlyJSONEncoder
4240

4341
from .app_name import app_name, main_view_label
4442
from .middleware import EmbeddedHolder
45-
46-
from .util import static_asset_path
4743
from .util import serve_locally as serve_locally_setting
4844
from .util import stateless_app_lookup_hook
45+
from .util import static_asset_path
4946

5047
try:
5148
from dataclasses import dataclass
52-
from typing import Dict, List
5349

5450
@dataclass(frozen=True)
5551
class CallbackContext:
@@ -201,7 +197,6 @@ def __init__(self, name=None, serve_locally=None,
201197

202198
if self._serve_locally:
203199
# Ensure package is loaded; if not present then pip install dpd-static-support
204-
import dpd_static_support
205200
hard_coded_package_name = "dpd_static_support"
206201
base_file_name = bootstrap_source.split('/')[-1]
207202

@@ -705,7 +700,20 @@ def dispatch_with_args(self, body, argMap):
705700
res = callback(*args, **argMap)
706701

707702
if da:
708-
root_value = json.loads(res).get('response', {})
703+
class LazyJson:
704+
"""A class to allow delayed the evaluation of a dict (returned by `func`)
705+
till the first get(...) is called on the dict."""
706+
707+
def __init__(self, func):
708+
self._root_value = func
709+
710+
def get(self, item, default):
711+
if isinstance(self._root_value, Callable):
712+
self._root_value = self._root_value()
713+
return self._root_value.get(item, default)
714+
715+
# wraps the json parsing of the response into LazyJson to avoid unnecessary parsing
716+
root_value = LazyJson(lambda: json.loads(res).get('response', {}))
709717

710718
for output_item in outputs:
711719
if isinstance(output_item, str):

django_plotly_dash/tests.py

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -79,33 +79,37 @@ def test_dash_stateful_app_client_contract(client):
7979

8080
# set the initial expected state
8181
expected_state = {'inp1': {'n_clicks': 0, 'n_clicks_timestamp': 1611733453854},
82-
'inp2': {'n_clicks': 5, 'n_clicks_timestamp': 1611733454354},
82+
'inp1b': {'n_clicks': 5, 'n_clicks_timestamp': 1611733454354},
83+
'inp2': {'n_clicks': 7, 'n_clicks_timestamp': 1611733454554},
8384
'out1-0': {'n_clicks': 1, 'n_clicks_timestamp': 1611733453954},
8485
'out1-1': {'n_clicks': 2, 'n_clicks_timestamp': 1611733454054},
8586
'out1-2': {'n_clicks': 3, 'n_clicks_timestamp': 1611733454154},
8687
'out1-3': {'n_clicks': 4, 'n_clicks_timestamp': 1611733454254},
87-
'out2-0': {'n_clicks': 6, 'n_clicks_timestamp': 1611733454454},
88-
'out3': {'n_clicks': 10, 'n_clicks_timestamp': 1611733454854},
89-
'out4': {'n_clicks': 14, 'n_clicks_timestamp': 1611733455254},
90-
'out5': {'n_clicks': 18, 'n_clicks_timestamp': 1611733455654},
91-
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 7,
92-
'n_clicks_timestamp': 1611733454554},
93-
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 11,
94-
'n_clicks_timestamp': 1611733454954},
95-
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 15,
96-
'n_clicks_timestamp': 1611733455354},
97-
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 8,
98-
'n_clicks_timestamp': 1611733454654},
99-
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 12,
100-
'n_clicks_timestamp': 1611733455054},
101-
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 16,
102-
'n_clicks_timestamp': 1611733455454},
103-
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 9,
88+
'out1b': {'href': 'http://www.example.com/null',
89+
'n_clicks': 6,
90+
'n_clicks_timestamp': 1611733454454},
91+
'out2-0': {'n_clicks': 8, 'n_clicks_timestamp': 1611733454654},
92+
'out3': {'n_clicks': 12, 'n_clicks_timestamp': 1611733455054},
93+
'out4': {'n_clicks': 16, 'n_clicks_timestamp': 1611733455454},
94+
'out5': {'n_clicks': 20, 'n_clicks_timestamp': 1611733455854},
95+
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 9,
10496
'n_clicks_timestamp': 1611733454754},
105-
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 13,
97+
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 13,
10698
'n_clicks_timestamp': 1611733455154},
107-
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 17,
108-
'n_clicks_timestamp': 1611733455554}}
99+
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 17,
100+
'n_clicks_timestamp': 1611733455554},
101+
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 10,
102+
'n_clicks_timestamp': 1611733454854},
103+
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 14,
104+
'n_clicks_timestamp': 1611733455254},
105+
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 18,
106+
'n_clicks_timestamp': 1611733455654},
107+
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 11,
108+
'n_clicks_timestamp': 1611733454954},
109+
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 15,
110+
'n_clicks_timestamp': 1611733455354},
111+
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 19,
112+
'n_clicks_timestamp': 1611733455754}}
109113

110114
########## test state management of the app and conversion of components ids
111115
# search for state values in dash layout
@@ -122,7 +126,7 @@ def test_dash_stateful_app_client_contract(client):
122126

123127
# update an existent state => update current_state
124128
stateful_a.update_current_state('{"_id":"inp-2","_type":"btn5"}', "n_clicks", 100)
125-
expected_state['{"_id":"inp-2","_type":"btn5"}'] = {'n_clicks': 100, 'n_clicks_timestamp': 1611733455554}
129+
expected_state['{"_id":"inp-2","_type":"btn5"}'] = {'n_clicks': 100, 'n_clicks_timestamp': 1611733455754}
126130
assert stateful_a.current_state() == expected_state
127131

128132
assert DashApp.objects.get(instance_name="Some name").current_state() == {}
@@ -170,34 +174,38 @@ def test_dash_stateful_app_client_contract(client):
170174
stateful_a.handle_current_state()
171175

172176
# check final state has been changed accordingly
173-
final_state = {'inp1': {'n_clicks': 1, 'n_clicks_timestamp': 1611736145932},
174-
'inp2': {'n_clicks': 6, 'n_clicks_timestamp': 1611736146875},
177+
final_state = {'inp1': {'n_clicks': 1, 'n_clicks_timestamp': 1615103027288},
178+
'inp1b': {'n_clicks': 5, 'n_clicks_timestamp': 1615103033482},
179+
'inp2': {'n_clicks': 8, 'n_clicks_timestamp': 1615103036591},
175180
'out1-0': {'n_clicks': 1, 'n_clicks_timestamp': 1611733453954},
176181
'out1-1': {'n_clicks': 2, 'n_clicks_timestamp': 1611733454054},
177182
'out1-2': {'n_clicks': 3, 'n_clicks_timestamp': 1611733454154},
178183
'out1-3': {'n_clicks': 4, 'n_clicks_timestamp': 1611733454254},
179-
'out2-0': {'n_clicks': 6, 'n_clicks_timestamp': 1611733454454},
180-
'out3': {'n_clicks': 10, 'n_clicks_timestamp': 1611733454854},
181-
'out4': {'n_clicks': 14, 'n_clicks_timestamp': 1611733455254},
182-
'out5': {'n_clicks': 18, 'n_clicks_timestamp': 1611733455654},
183-
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 8,
184-
'n_clicks_timestamp': 1611736147644},
185-
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 12,
186-
'n_clicks_timestamp': 1611733454954},
187-
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 16,
188-
'n_clicks_timestamp': 1611733455354},
189-
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 9,
190-
'n_clicks_timestamp': 1611736148172},
191-
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 13,
192-
'n_clicks_timestamp': 1611733455054},
193-
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 18,
194-
'n_clicks_timestamp': 1611733455454},
195-
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 10,
196-
'n_clicks_timestamp': 1611736149140},
197-
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 13,
184+
'out1b': {'href': 'http://www.example.com/1615103033482',
185+
'n_clicks': 6,
186+
'n_clicks_timestamp': 1611733454454},
187+
'out2-0': {'n_clicks': 8, 'n_clicks_timestamp': 1611733454654},
188+
'out3': {'n_clicks': 12, 'n_clicks_timestamp': 1611733455054},
189+
'out4': {'n_clicks': 16, 'n_clicks_timestamp': 1611733455454},
190+
'out5': {'n_clicks': 20, 'n_clicks_timestamp': 1611733455854},
191+
'{"_id":"inp-0","_type":"btn3"}': {'n_clicks': 10,
192+
'n_clicks_timestamp': 1615103039030},
193+
'{"_id":"inp-0","_type":"btn4"}': {'n_clicks': 14,
198194
'n_clicks_timestamp': 1611733455154},
199-
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 19,
200-
'n_clicks_timestamp': 1611733455554}}
195+
'{"_id":"inp-0","_type":"btn5"}': {'n_clicks': 18,
196+
'n_clicks_timestamp': 1611733455554},
197+
'{"_id":"inp-1","_type":"btn3"}': {'n_clicks': 11,
198+
'n_clicks_timestamp': 1615103039496},
199+
'{"_id":"inp-1","_type":"btn4"}': {'n_clicks': 15,
200+
'n_clicks_timestamp': 1611733455254},
201+
'{"_id":"inp-1","_type":"btn5"}': {'n_clicks': 19,
202+
'n_clicks_timestamp': 1611733455654},
203+
'{"_id":"inp-2","_type":"btn3"}': {'n_clicks': 12,
204+
'n_clicks_timestamp': 1615103040528},
205+
'{"_id":"inp-2","_type":"btn4"}': {'n_clicks': 15,
206+
'n_clicks_timestamp': 1611733455354},
207+
'{"_id":"inp-2","_type":"btn5"}': {'n_clicks': 20,
208+
'n_clicks_timestamp': 1611733455754}}
201209

202210
assert DashApp.objects.get(instance_name="Some name").current_state() == final_state
203211

0 commit comments

Comments
 (0)