2424OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2525SOFTWARE.
2626'''
27-
27+ import itertools
2828import json
2929import inspect
3030
@@ -160,7 +160,6 @@ class DjangoDash:
160160 '''
161161 #pylint: disable=too-many-instance-attributes
162162 def __init__ (self , name = None , serve_locally = None ,
163- expanded_callbacks = False ,
164163 add_bootstrap_links = False ,
165164 suppress_callback_exceptions = False ,
166165 ** kwargs ): # pylint: disable=unused-argument, too-many-arguments
@@ -186,7 +185,6 @@ def __init__(self, name=None, serve_locally=None,
186185 else :
187186 self ._serve_locally = serve_locally
188187
189- self ._expanded_callbacks = expanded_callbacks
190188 self ._suppress_callback_exceptions = suppress_callback_exceptions
191189
192190 if add_bootstrap_links :
@@ -268,7 +266,6 @@ def form_dash_instance(self, replacements=None, ndid=None, base_pathname=None):
268266 ndid = self ._uid
269267
270268 rd = WrappedDash (base_pathname = base_pathname ,
271- expanded_callbacks = self ._expanded_callbacks ,
272269 replacements = replacements ,
273270 ndid = ndid ,
274271 serve_locally = self ._serve_locally )
@@ -287,26 +284,58 @@ def form_dash_instance(self, replacements=None, ndid=None, base_pathname=None):
287284
288285 return rd
289286
287+ @staticmethod
288+ def get_expanded_arguments (func , inputs , state ):
289+ """Analyse a callback function signature to detect the expanded arguments to add when called.
290+ It uses the inputs and the state information to identify what arguments are already coming from Dash.
291+
292+ It returns a list of the expanded parameters to inject (can be [] if nothing should be injected)
293+ or None if all parameters should be injected."""
294+ n_dash_parameters = len (inputs or []) + len (state or [])
295+
296+ parameter_types = {kind : [p .name for p in parameters ] for kind , parameters in
297+ itertools .groupby (inspect .signature (func ).parameters .values (), lambda p : p .kind )}
298+ if inspect .Parameter .VAR_KEYWORD in parameter_types :
299+ # there is some **kwargs, inject all parameters
300+ expanded = None
301+ elif inspect .Parameter .VAR_POSITIONAL in parameter_types :
302+ # there is a *args, assume all parameters afterwards (KEYWORD_ONLY) are to be injected
303+ # some of these parameters may not be expanded arguments but that is ok
304+ expanded = parameter_types .get (inspect .Parameter .KEYWORD_ONLY , [])
305+ else :
306+ # there is no **kwargs, filter argMap to take only the keyword arguments
307+ expanded = parameter_types .get (inspect .Parameter .POSITIONAL_OR_KEYWORD , [])[
308+ n_dash_parameters :] + parameter_types .get (inspect .Parameter .KEYWORD_ONLY , [])
309+
310+ return expanded
311+
290312 def callback (self , output , inputs = None , state = None , events = None ):
291- 'Form a callback function by wrapping, in the same way as the underlying Dash application would'
292- callback_set = {'output' :output ,
293- 'inputs' :inputs and inputs or dict (),
294- 'state' :state and state or dict (),
295- 'events' :events and events or dict ()}
296- def wrap_func (func , callback_set = callback_set , callback_sets = self ._callback_sets ): # pylint: disable=dangerous-default-value, missing-docstring
297- callback_sets .append ((callback_set , func ))
298- return func
299- return wrap_func
313+ '''Form a callback function by wrapping, in the same way as the underlying Dash application would
314+ but handling extra arguments provided by dpd.
300315
301- def expanded_callback (self , output , inputs = [], state = [], events = []): # pylint: disable=dangerous-default-value
302- '''
303- Form an expanded callback.
316+ It will inspect the signature of the function to ensure only relevant expanded arguments are passed to the callback.
317+
318+ If the function accepts a **kwargs => all expanded arguments are sent to the function in the kwargs.
319+ If the function has a *args => expanded arguments matching parameters after the *args are injected.
320+ Otherwise, take all arguments beyond the one provided by Dash (based on the Inputs/States provided).
304321
305- This function registers the callback function, and sets an internal flag that mandates that all
306- callbacks are passed the enhanced arguments.
307322 '''
308- self ._expanded_callbacks = True
309- return self .callback (output , inputs , state , events )
323+ callback_set = {'output' : output ,
324+ 'inputs' : inputs or [],
325+ 'state' : state or [],
326+ 'events' : events or []}
327+
328+ def wrap_func (func ):
329+ self ._callback_sets .append ((callback_set , func ))
330+ # add an expanded attribute to the function with the information to use in dispatch_with_args
331+ # to inject properly only the expanded arguments the function can accept
332+ # if .expanded is None => inject all
333+ # if .expanded is a list => inject only
334+ func .expanded = DjangoDash .get_expanded_arguments (func , inputs , state )
335+ return func
336+ return wrap_func
337+
338+ expanded_callback = callback
310339
311340 def clientside_callback (self , clientside_function , output , inputs = None , state = None ):
312341 'Form a callback function by wrapping, in the same way as the underlying Dash application would'
@@ -358,8 +387,7 @@ class WrappedDash(Dash):
358387 'Wrapper around the Plotly Dash application instance'
359388 # pylint: disable=too-many-arguments, too-many-instance-attributes
360389 def __init__ (self ,
361- base_pathname = None , replacements = None , ndid = None ,
362- expanded_callbacks = False , serve_locally = False ,
390+ base_pathname = None , replacements = None , ndid = None , serve_locally = False ,
363391 ** kwargs ):
364392
365393 self ._uid = ndid
@@ -378,7 +406,6 @@ def __init__(self,
378406 self .scripts .config .serve_locally = serve_locally
379407
380408 self ._adjust_id = False
381- self ._dash_dispatch = not expanded_callbacks
382409 if replacements :
383410 self ._replacements = replacements
384411 else :
@@ -387,10 +414,6 @@ def __init__(self,
387414
388415 self ._return_embedded = False
389416
390- def use_dash_dispatch (self ):
391- 'Indicate if dispatch is using underlying dash code or the wrapped code'
392- return self ._dash_dispatch
393-
394417 def use_dash_layout (self ):
395418 '''
396419 Indicate if the underlying dash layout can be used.
@@ -630,7 +653,14 @@ def dispatch_with_args(self, body, argMap):
630653 if len (args ) < len (callback_info ['inputs' ]):
631654 return 'EDGECASEEXIT'
632655
633- res = callback_info ['callback' ](* args , ** argMap )
656+ callback = callback_info ["callback" ]
657+ # smart injection of parameters if .expanded is defined
658+ if callback .expanded is not None :
659+ parameters_to_inject = {* callback .expanded , 'outputs_list' }
660+ res = callback (* args , ** {k : v for k , v in argMap .items () if k in parameters_to_inject })
661+ else :
662+ res = callback (* args , ** argMap )
663+
634664 if da :
635665 root_value = json .loads (res ).get ('response' , {})
636666
0 commit comments