I agree that callbacks with shared outputs would be a giant step forward for Dash. Combining all inputs affecting to one output results in messy code, and is certainly more error prone than dividing the code into smaller logical blocks.
This issue has been discussed in #153 by @chriddyp, @alexcjohnson and others. I think that in this case the python rule āthere should be one obvious way to do thingsā would mean shared outputs instead of digging triggered inputs from callback_context
.
There was question what if there is multiple callbacks with both a shared input and a shared output. This could definitely be an error, but could most of the errors be prevented if findDuplicateOutputs
in dependencies.js was modified to something that just prevents having both a same input and an output in two or more callbacks?
Separating the triggered inputs is not the only problem of the CantHaveMultipleOutputs
limit. For example, one of my Dash-based projects has currently over 60 callbacks, and I would like to wrap them all in a try-except block and inform the user if something went wrong in a callback.
I was able to implement this behavior by appending a dummy output to each callback (using data-*
attributes), collecting all these outputs in one callback and redirecting the messages to a ConfirmDialog. However, this method gives 60+ missing input warnings if the callback exceptions are not suppressed and results in a messy callback graph.
As an alternative approach, I modified the original Dash callback decorator to something like shown below. I was able to redirect errors into a single ConfirmDialog from all the callbacks.
def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
multi = isinstance(output, (list, tuple))
def wrap_func(func):
@wraps(func)
def add_context(*args, **kwargs):
output_spec = kwargs.pop("outputs_list")
# ADDED: output value and error message initially set to none
output_value = None
error_message = None
# ADDED; try-except block
try:
# don't touch the comment on the next line - used by debugger
output_value = func(*args, **kwargs) # %% callback invoked %%
except PreventUpdate:
raise PreventUpdate
#except UserError as e:
# UserErrors are raised intentionally inside callbacks to inform the user about wrong input etc.
# error_message = str(e)
except Exception as e:
error_message = f'There was an unexcepted error! {e.__class__.__name__}'
# MOVED component_ids initialisation here
component_ids = collections.defaultdict(dict)
# ADDED: error message output to a ConfirmDialog
if error_message:
component_ids['error-output']['message'] = error_message
component_ids['error-output']['displayed'] = True
# ADDED this check as output_value may be None due to an exception
if output_value:
if isinstance(output_value, _NoUpdate):
raise PreventUpdate
# wrap single outputs so we can treat them all the same
# for validation and response creation
if not multi:
output_value, output_spec = [output_value], [output_spec]
_validate.validate_multi_return(output_spec, output_value, callback_id)
has_update = False
for val, spec in zip(output_value, output_spec):
if isinstance(val, _NoUpdate):
continue
for vali, speci in (
zip(val, spec) if isinstance(spec, list) else [[val, spec]]
):
if not isinstance(vali, _NoUpdate):
has_update = True
id_str = stringify_id(speci["id"])
component_ids[id_str][speci["property"]] = vali
# MODIFIED to not raise PreventUpdate if there is error output
# but no other outputs. Is `has_updateĀ“ required anyway?
if len(component_ids) == 0:
raise PreventUpdate
response = {"response": component_ids, "multi": True}
try:
jsonResponse = json.dumps(
response, cls=plotly.utils.PlotlyJSONEncoder
)
except TypeError:
_validate.fail_callback_output(output_value, output)
return jsonResponse
self.callback_map[callback_id]["callback"] = add_context
return add_context
return wrap_func
Similarly, I implemented a simple method to give app users some feedback: adding a method like below to Dash class and calling app.inform('It worked!')
inside a callback appends the message to a list in flask.g
, and after each callback, the messages can be collected and prompted to the user.
def inform(self, message):
if not 'info' in flask.g:
flask.g.info = []
flask.g.info.append(message)
So it seems that sharing an output between callbacks is already possible with very minor changes. However, callbacks chained with these additional outputs are not working just like that ![:slight_smile: :slight_smile:](https://emoji.discourse-cdn.com/twitter/slight_smile.png?v=9)
I have a vision that with a similar method, from any Dash callback, one could update any component visible in the DOM. It could work by renaming callback_ids
to something more descriptive (response_data
?) and making it available through flask.g
(or callback_context
) before executing the wrapped callback code. This way, one could call something like app.response_data['my-desired-output']['value'] = 100
or app.set_output('my-desired-output', 'value', 100)
in a callback, and the component should be updated (if present).
Is there any fundamental reason why callback outputs should even be known beforehand? Isnāt that just some response json from which dash-render deduces which component states should be updated by React? After an update, all changed properties are collected and all callbacks having these properties as inputs are eventually called. So, the inputs are the ones which define the callbacks, not the outputs.
Anyhow, thank you for your great work! We use Dash to visualize and analyze measurement data, and it really spares us repeating ourselves in all kinds of excels ![:relaxed: :relaxed:](https://emoji.discourse-cdn.com/twitter/relaxed.png?v=9)