12
12
import warnings
13
13
import re
14
14
import logging
15
+ import pprint
15
16
16
17
from functools import wraps
17
18
30
31
from ._utils import interpolate_str as _interpolate
31
32
from ._utils import format_tag as _format_tag
32
33
from ._utils import generate_hash as _generate_hash
33
- from ._utils import get_asset_path as _get_asset_path
34
34
from ._utils import patch_collections_abc as _patch_collections_abc
35
35
from . import _watch
36
+ from ._utils import get_asset_path as _get_asset_path
37
+ from ._utils import create_callback_id as _create_callback_id
36
38
from . import _configs
37
39
38
40
_default_index = '''<!DOCTYPE html>
@@ -168,6 +170,9 @@ def __init__(
168
170
self ._meta_tags = meta_tags or []
169
171
self ._favicon = None
170
172
173
+ # default renderer string
174
+ self .renderer = 'var renderer = new DashRenderer();'
175
+
171
176
if compress :
172
177
# gzip
173
178
Compress (self .server )
@@ -191,47 +196,36 @@ def _handle_error(_):
191
196
# urls
192
197
self .routes = []
193
198
194
- self ._add_url (
195
- '{}_dash-layout' .format (self .config ['routes_pathname_prefix' ]),
196
- self .serve_layout )
199
+ prefix = self .config ['routes_pathname_prefix' ]
197
200
198
- self ._add_url (
199
- '{}_dash-dependencies' .format (
200
- self .config ['routes_pathname_prefix' ]),
201
- self .dependencies )
201
+ self ._add_url ('{}_dash-layout' .format (prefix ), self .serve_layout )
202
+
203
+ self ._add_url ('{}_dash-dependencies' .format (prefix ), self .dependencies )
202
204
203
205
self ._add_url (
204
- '{}_dash-update-component' .format (
205
- self .config ['routes_pathname_prefix' ]),
206
+ '{}_dash-update-component' .format (prefix ),
206
207
self .dispatch ,
207
208
['POST' ])
208
209
209
- self ._add_url ((
210
- '{}_dash-component-suites'
211
- '/<string:package_name>'
212
- '/<path:path_in_package_dist>' ).format (
213
- self .config ['routes_pathname_prefix' ]),
214
- self .serve_component_suites )
215
-
216
210
self ._add_url (
217
- '{}_dash-routes' .format (self .config ['routes_pathname_prefix' ]),
218
- self .serve_routes )
211
+ (
212
+ '{}_dash-component-suites'
213
+ '/<string:package_name>'
214
+ '/<path:path_in_package_dist>'
215
+ ).format (prefix ),
216
+ self .serve_component_suites )
219
217
220
- self ._add_url (
221
- self .config ['routes_pathname_prefix' ],
222
- self .index )
218
+ self ._add_url ('{}_dash-routes' .format (prefix ), self .serve_routes )
223
219
224
- self ._add_url (
225
- '{}_reload-hash' . format ( self . config [ 'routes_pathname_prefix' ]),
226
- self .serve_reload_hash )
220
+ self ._add_url (prefix , self . index )
221
+
222
+ self . _add_url ( '{}_reload-hash' . format ( prefix ), self .serve_reload_hash )
227
223
228
224
# catch-all for front-end routes, used by dcc.Location
229
- self ._add_url (
230
- '{}<path:path>' .format (self .config ['routes_pathname_prefix' ]),
231
- self .index )
225
+ self ._add_url ('{}<path:path>' .format (prefix ), self .index )
232
226
233
227
self ._add_url (
234
- '{}_favicon.ico' .format (self . config [ 'routes_pathname_prefix' ] ),
228
+ '{}_favicon.ico' .format (prefix ),
235
229
self ._serve_default_favicon )
236
230
237
231
self .server .before_first_request (self ._setup_server )
@@ -273,9 +267,6 @@ def _add_url(self, name, view_func, methods=('GET',)):
273
267
# e.g. for adding authentication with flask_login
274
268
self .routes .append (name )
275
269
276
- # default renderer string
277
- self .renderer = 'var renderer = new DashRenderer();'
278
-
279
270
@property
280
271
def layout (self ):
281
272
return self ._layout
@@ -637,10 +628,7 @@ def interpolate_index(self, **kwargs):
637
628
def dependencies (self ):
638
629
return flask .jsonify ([
639
630
{
640
- 'output' : {
641
- 'id' : k .split ('.' )[0 ],
642
- 'property' : k .split ('.' )[1 ]
643
- },
631
+ 'output' : k ,
644
632
'inputs' : v ['inputs' ],
645
633
'state' : v ['state' ],
646
634
} for k , v in self .callback_map .items ()
@@ -656,11 +644,33 @@ def react(self, *args, **kwargs):
656
644
def _validate_callback (self , output , inputs , state ):
657
645
# pylint: disable=too-many-branches
658
646
layout = self ._cached_layout or self ._layout_value ()
647
+ is_multi = isinstance (output , (list , tuple ))
659
648
660
649
for i in inputs :
661
- if output == i :
650
+ bad = None
651
+ if is_multi :
652
+ for o in output :
653
+ if o == i :
654
+ bad = o
655
+ else :
656
+ if output == i :
657
+ bad = output
658
+ if bad :
662
659
raise exceptions .SameInputOutputException (
663
- 'Same output and input: {}' .format (output )
660
+ 'Same output and input: {}' .format (bad )
661
+ )
662
+
663
+ if is_multi :
664
+ if len (set (output )) != len (output ):
665
+ raise exceptions .DuplicateCallbackOutput (
666
+ 'Same output was used in a'
667
+ ' multi output callback!\n Duplicates:\n {}' .format (
668
+ ',\n ' .join (
669
+ k for k , v in
670
+ ((str (x ), output .count (x )) for x in output )
671
+ if v > 1
672
+ )
673
+ )
664
674
)
665
675
666
676
if (layout is None and
@@ -676,7 +686,10 @@ def _validate_callback(self, output, inputs, state):
676
686
`app.config['suppress_callback_exceptions']=True`
677
687
''' .replace (' ' , '' ))
678
688
679
- for args , obj , name in [([output ], Output , 'Output' ),
689
+ for args , obj , name in [(output if isinstance (output , (list , tuple ))
690
+ else [output ],
691
+ (Output , list , tuple ),
692
+ 'Output' ),
680
693
(inputs , Input , 'Input' ),
681
694
(state , State , 'State' )]:
682
695
@@ -695,6 +708,15 @@ def _validate_callback(self, output, inputs, state):
695
708
name .lower (), str (arg ), name
696
709
))
697
710
711
+ invalid_characters = ['.' ]
712
+ if any (x in arg .component_id for x in invalid_characters ):
713
+ raise exceptions .InvalidComponentIdError ('''The element
714
+ `{}` contains {} in its ID.
715
+ Periods are not allowed in IDs right now.''' .format (
716
+ arg .component_id ,
717
+ invalid_characters
718
+ ))
719
+
698
720
if (not self .config .first ('suppress_callback_exceptions' ,
699
721
'supress_callback_exceptions' ) and
700
722
arg .component_id not in layout and
@@ -765,24 +787,48 @@ def _validate_callback(self, output, inputs, state):
765
787
'elements' if len (state ) > 1 else 'element'
766
788
).replace (' ' , '' ))
767
789
768
- if '.' in output .component_id :
769
- raise exceptions .IDsCantContainPeriods ('''The Output element
770
- `{}` contains a period in its ID.
771
- Periods are not allowed in IDs right now.''' .format (
772
- output .component_id
773
- ))
774
-
775
- callback_id = '{}.{}' .format (
776
- output .component_id , output .component_property )
777
- if callback_id in self .callback_map :
778
- raise exceptions .CantHaveMultipleOutputs ('''
790
+ callback_id = _create_callback_id (output )
791
+
792
+ callbacks = set (itertools .chain (* (
793
+ x [2 :- 2 ].split ('...' )
794
+ if x .startswith ('..' )
795
+ else [x ]
796
+ for x in self .callback_map
797
+ )))
798
+ ns = {
799
+ 'duplicates' : set ()
800
+ }
801
+ if is_multi :
802
+ def duplicate_check ():
803
+ ns ['duplicates' ] = callbacks .intersection (
804
+ str (y ) for y in output
805
+ )
806
+ return ns ['duplicates' ]
807
+ else :
808
+ def duplicate_check ():
809
+ return callback_id in callbacks
810
+ if duplicate_check ():
811
+ if is_multi :
812
+ msg = '''
813
+ Multi output {} contains an `Output` object
814
+ that was already assigned.
815
+ Duplicates:
816
+ {}
817
+ ''' .format (
818
+ callback_id ,
819
+ pprint .pformat (ns ['duplicates' ])
820
+ )
821
+ else :
822
+ msg = '''
779
823
You have already assigned a callback to the output
780
824
with ID "{}" and property "{}". An output can only have
781
825
a single callback function. Try combining your inputs and
782
826
callback functions together into one function.
783
- ''' .format (
784
- output .component_id ,
785
- output .component_property ).replace (' ' , '' ))
827
+ ''' .format (
828
+ output .component_id ,
829
+ output .component_property
830
+ ).replace (' ' , '' )
831
+ raise exceptions .DuplicateCallbackOutput (msg )
786
832
787
833
def _validate_callback_output (self , output_value , output ):
788
834
valid = [str , dict , int , float , type (None ), Component ]
@@ -904,9 +950,9 @@ def _validate_value(val, index=None):
904
950
def callback (self , output , inputs = [], state = []):
905
951
self ._validate_callback (output , inputs , state )
906
952
907
- callback_id = '{}.{}' . format (
908
- output . component_id , output . component_property
909
- )
953
+ callback_id = _create_callback_id ( output )
954
+ multi = isinstance ( output , ( list , tuple ))
955
+
910
956
self .callback_map [callback_id ] = {
911
957
'inputs' : [
912
958
{'id' : c .component_id , 'property' : c .component_property }
@@ -921,15 +967,44 @@ def callback(self, output, inputs=[], state=[]):
921
967
def wrap_func (func ):
922
968
@wraps (func )
923
969
def add_context (* args , ** kwargs ):
924
-
925
970
output_value = func (* args , ** kwargs )
926
- response = {
927
- 'response' : {
928
- 'props' : {
929
- output .component_property : output_value
971
+ if multi :
972
+ if not isinstance (output_value , (list , tuple )):
973
+ raise exceptions .InvalidCallbackReturnValue (
974
+ 'The callback {} is a multi-output.\n '
975
+ 'Expected the output type to be a list'
976
+ ' or tuple but got {}.' .format (
977
+ callback_id , repr (output_value )
978
+ )
979
+ )
980
+
981
+ if not len (output_value ) == len (output ):
982
+ raise exceptions .InvalidCallbackReturnValue (
983
+ 'Invalid number of output values for {}.\n '
984
+ ' Expected {} got {}' .format (
985
+ callback_id ,
986
+ len (output ),
987
+ len (output_value )
988
+ )
989
+ )
990
+
991
+ component_ids = collections .defaultdict (dict )
992
+ for i , o in enumerate (output ):
993
+ component_ids [o .component_id ][o .component_property ] = \
994
+ output_value [i ]
995
+
996
+ response = {
997
+ 'response' : component_ids ,
998
+ 'multi' : True
999
+ }
1000
+ else :
1001
+ response = {
1002
+ 'response' : {
1003
+ 'props' : {
1004
+ output .component_property : output_value
1005
+ }
930
1006
}
931
1007
}
932
- }
933
1008
934
1009
try :
935
1010
jsonResponse = json .dumps (
@@ -966,7 +1041,6 @@ def dispatch(self):
966
1041
state = body .get ('state' , [])
967
1042
output = body ['output' ]
968
1043
969
- target_id = '{}.{}' .format (output ['id' ], output ['property' ])
970
1044
args = []
971
1045
972
1046
flask .g .input_values = input_values = {
@@ -983,21 +1057,21 @@ def dispatch(self):
983
1057
for x in changed_props
984
1058
] if changed_props else []
985
1059
986
- for component_registration in self .callback_map [target_id ]['inputs' ]:
1060
+ for component_registration in self .callback_map [output ]['inputs' ]:
987
1061
args .append ([
988
1062
c .get ('value' , None ) for c in inputs if
989
1063
c ['property' ] == component_registration ['property' ] and
990
1064
c ['id' ] == component_registration ['id' ]
991
1065
][0 ])
992
1066
993
- for component_registration in self .callback_map [target_id ]['state' ]:
1067
+ for component_registration in self .callback_map [output ]['state' ]:
994
1068
args .append ([
995
1069
c .get ('value' , None ) for c in state if
996
1070
c ['property' ] == component_registration ['property' ] and
997
1071
c ['id' ] == component_registration ['id' ]
998
1072
][0 ])
999
1073
1000
- return self .callback_map [target_id ]['callback' ](* args )
1074
+ return self .callback_map [output ]['callback' ](* args )
1001
1075
1002
1076
def _validate_layout (self ):
1003
1077
if self .layout is None :
0 commit comments