Skip to content

Commit f410c48

Browse files
fix #305: bug when output is list of one Output (#308)
* fix #305: - simplify single_case vs multiple_case handling - refactor calls to self.callback_map[target_id] - adapt tests - (misc) add missing dev dep on dash-bootstrap-components * fix bug in reading response in the single_case * remove dead code as according to https://github.com/plotly/dash/blob/dev/dash/dash.py#L1037, dash always returns a multi:true response Co-authored-by: GFJ138 <sebastien.dementen@engie.com>
1 parent 03f8554 commit f410c48

File tree

4 files changed

+81
-54
lines changed

4 files changed

+81
-54
lines changed

demo/demo/dash_apps.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
className='col-md-12',
5151
),
5252

53-
html.Div(id='test-output-div2')
53+
html.Div(id='test-output-div2'),
54+
html.Div(id='test-output-div3')
5455

5556
]) # end of 'main'
5657

@@ -100,3 +101,35 @@ def callback_test2(*args, **kwargs):
100101
html.Div(["The session context message is '%s'" %(kwargs['session_state']['django_to_dash_context'])])]
101102

102103
return children
104+
105+
@dash_example1.expanded_callback(
106+
[dash.dependencies.Output('test-output-div3', 'children')],
107+
[dash.dependencies.Input('my-dropdown1', 'value')])
108+
def callback_test(*args, **kwargs): #pylint: disable=unused-argument
109+
'Callback to generate test data on each change of the dropdown'
110+
111+
# Creating a random Graph from a Plotly example:
112+
N = 500
113+
random_x = np.linspace(0, 1, N)
114+
random_y = np.random.randn(N)
115+
116+
# Create a trace
117+
trace = go.Scatter(x=random_x,
118+
y=random_y)
119+
120+
data = [trace]
121+
122+
layout = dict(title='',
123+
yaxis=dict(zeroline=False, title='Total Expense (£)',),
124+
xaxis=dict(zeroline=False, title='Date', tickangle=0),
125+
margin=dict(t=20, b=50, l=50, r=40),
126+
height=350,
127+
)
128+
129+
130+
fig = dict(data=data, layout=layout)
131+
line_graph = dcc.Graph(id='line-area-graph2', figure=fig, style={'display':'inline-block', 'width':'100%',
132+
'height':'100%;'})
133+
children = [line_graph]
134+
135+
return [children]

dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ coveralls>=1.6.0
44
channels>=2.0
55
channels-redis
66
daphne
7+
dash-bootstrap-components
78
Django>=2.0
89
django-bootstrap4
910
django-redis

django_plotly_dash/dash_wrapper.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -589,43 +589,29 @@ def dispatch_with_args(self, body, argMap):
589589
if len(argMap) > 0:
590590
argMap['callback_context'] = callback_context
591591

592-
outputs = []
593-
try:
594-
if output[:2] == '..' and output[-2:] == '..':
595-
# Multiple outputs
596-
outputs = output[2:-2].split('...')
597-
target_id = output
598-
# Special case of a single output
599-
if len(outputs) == 1:
600-
target_id = output[2:-2]
601-
except:
602-
pass
603-
604-
single_case = False
605-
if len(outputs) < 1:
606-
try:
607-
output_id = output['id']
608-
output_property = output['property']
609-
target_id = "%s.%s" %(output_id, output_property)
610-
except:
611-
target_id = output
612-
output_id, output_property = output.split(".")
613-
single_case = True
614-
outputs = [output,]
592+
single_case = not(output.startswith('..') and output.endswith('..'))
593+
if single_case:
594+
# single Output (not in a list)
595+
outputs = [output]
596+
else:
597+
# multiple outputs in a list (the list could contain a single item)
598+
outputs = output[2:-2].split('...')
615599

616600
args = []
617601

618602
da = argMap.get('dash_app', None)
619603

620-
for component_registration in self.callback_map[target_id]['inputs']:
604+
callback_info = self.callback_map[output]
605+
606+
for component_registration in callback_info['inputs']:
621607
for c in inputs:
622608
if c['property'] == component_registration['property'] and c['id'] == component_registration['id']:
623609
v = c.get('value', None)
624610
args.append(v)
625611
if da:
626612
da.update_current_state(c['id'], c['property'], v)
627613

628-
for component_registration in self.callback_map[target_id]['state']:
614+
for component_registration in callback_info['state']:
629615
for c in states:
630616
if c['property'] == component_registration['property'] and c['id'] == component_registration['id']:
631617
v = c.get('value', None)
@@ -638,21 +624,16 @@ def dispatch_with_args(self, body, argMap):
638624
argMap['outputs_list'] = outputs_list
639625

640626
# Special: intercept case of insufficient arguments
641-
# This happens when a propery has been updated with a pipe component
627+
# This happens when a property has been updated with a pipe component
642628
# TODO see if this can be attacked from the client end
643629

644-
if len(args) < len(self.callback_map[target_id]['inputs']):
630+
if len(args) < len(callback_info['inputs']):
645631
return 'EDGECASEEXIT'
646632

647-
res = self.callback_map[target_id]['callback'](*args, **argMap)
633+
res = callback_info['callback'](*args, **argMap)
648634
if da:
649-
if single_case and da.have_current_state_entry(output_id, output_property):
650-
response = json.loads(res.data.decode('utf-8'))
651-
value = response.get('response', {}).get('props', {}).get(output_property, None)
652-
da.update_current_state(output_id, output_property, value)
635+
root_value = json.loads(res).get('response', {})
653636

654-
response = json.loads(res)
655-
root_value = response.get('response', {})
656637
for output_item in outputs:
657638
if isinstance(output_item, str):
658639
output_id, output_property = output_item.split('.')

django_plotly_dash/tests.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,10 @@ def test_injection_updating(client):
221221

222222
rStart = b'{"response": {"test-output-div": {"children": [{"props": {"id": "line-area-graph2"'
223223

224-
assert response.content[:len(rStart)] == rStart
224+
assert response.content.startswith(rStart)
225225
assert response.status_code == 200
226226

227-
# New variant of output has a string used to name the properties
227+
# Single output callback, output=="component_id.component_prop"
228228
response = client.post(url, json.dumps({'output':'test-output-div.children',
229229
'inputs':[{'id':'my-dropdown1',
230230
'property':'value',
@@ -233,49 +233,61 @@ def test_injection_updating(client):
233233

234234
rStart = b'{"response": {"test-output-div": {"children": [{"props": {"id": "line-area-graph2"'
235235

236-
assert response.content[:len(rStart)] == rStart
236+
assert response.content.startswith(rStart)
237237
assert response.status_code == 200
238238

239-
# Second variant has a single-entry mulitple property output
240-
response = client.post(url, json.dumps({'output':'..test-output-div.children..',
239+
# Single output callback, fails if output=="..component_id.component_prop.."
240+
with pytest.raises(KeyError, match="..test-output-div.children.."):
241+
client.post(url, json.dumps({'output':'..test-output-div.children..',
241242
'inputs':[{'id':'my-dropdown1',
242243
'property':'value',
243244
'value':'TestIt'},
244245
]}), content_type="application/json")
245246

246-
rStart = b'{"response": {"test-output-div": {"children": {"props": {"id": "line-area-graph2"'
247247

248-
assert response.content[:len(rStart)] == rStart
249-
assert response.status_code == 200
248+
# Multiple output callback, fails if output=="component_id.component_prop"
249+
with pytest.raises(KeyError, match="test-output-div3.children"):
250+
client.post(url, json.dumps({'output':'test-output-div3.children',
251+
'inputs':[{'id':'my-dropdown1',
252+
'property':'value',
253+
'value':'TestIt'},
254+
]}), content_type="application/json")
250255

251-
have_thrown = False
252256

253-
try:
257+
# Multiple output callback, output=="..component_id.component_prop.."
258+
response = client.post(url, json.dumps({'output':'..test-output-div3.children..',
259+
'inputs':[{'id':'my-dropdown1',
260+
'property':'value',
261+
'value':'TestIt'},
262+
]}), content_type="application/json")
263+
264+
rStart = b'{"response": {"test-output-div3": {"children": [{"props": {"id": "line-area-graph2"'
265+
266+
assert response.content.startswith(rStart)
267+
assert response.status_code == 200
268+
269+
with pytest.raises(KeyError, match="django_to_dash_context"):
254270
client.post(url, json.dumps({'output': 'test-output-div2.children',
255271
'inputs':[{'id':'my-dropdown2',
256272
'property':'value',
257273
'value':'TestIt'},
258274
]}), content_type="application/json")
259-
except:
260-
have_thrown = True
261-
262-
assert have_thrown
263275

264276
session = client.session
265277
session['django_plotly_dash'] = {'django_to_dash_context': 'Test 789 content'}
266278
session.save()
267279

268-
response3 = client.post(url, json.dumps({'output': 'test-output-div2.children',
280+
response = client.post(url, json.dumps({'output': 'test-output-div2.children',
269281
'inputs':[{'id':'my-dropdown2',
270282
'property':'value',
271283
'value':'TestIt'},
272284
]}), content_type="application/json")
273-
rStart3 = b'{"response": {"test-output-div2": {"children": [{"props": {"children": ["You have '
285+
rStart = b'{"response": {"test-output-div2": {"children": [{"props": {"children": ["You have '
274286

275-
assert response3.content[:len(rStart3)] == rStart3
276-
assert response3.status_code == 200
287+
assert response.content.startswith(rStart)
288+
assert response.status_code == 200
277289

278-
assert response3.content.find(b'Test 789 content') > 0
290+
assert response.content.find(b'Test 789 content') > 0
279291

280292

281293
@pytest.mark.django_db

0 commit comments

Comments
 (0)