Duplicate output callbacks across Python and JS namespaces fail if both set to allow_duplicate #3120
Description
Describe the bug
Duplicate output callbacks across Python and JavaScript namespaces fail if both set to allow_duplicate
. They work, and allow parallel runs, if only one is enabled, and the other disabled.
This may be related to #2486, but I believe these specific examples indicates more of a bug than expected behavior. The reason I say this is a "bug", is because it does technically work, even with the same Input/Output combo, but only if you do not put allow_duplicate
on both. If you set it on either/or, then you are allowed to overlap it successfully. If you set it on both, then it complains that it needs to be set, even though it is set (confusing, regardless of final outcome of this ask).
Additionally, while I will demonstrate this occurs with matching outputs, it does also happen if the output combo is not the same (but the input is).
Expected behavior
If there are two callbacks (one python, and one javascript), even if matching signatures, they should not be considered duplicates, in order to maximize client application performance and flexibility.
Environment
Happens on older and current versions. Tested on latest as of:
dash 2.18.2
dash-core-components 2.0.0
dash-html-components 2.0.0
dash-table 5.0.0
Examples
Minimum repo:
import time
import dash
from dash import Input
from dash import Output
from dash import html
app = dash.Dash(__name__)
app.layout = html.Div([
html.Button('Button', id='btn-py'),
html.Div('Press a button', id='output'),
])
app.clientside_callback(
"""function jsCallback(nClicks) {return `JavaScript called ${nClicks} times`;}""",
# Output("output", "children"),
Output("output", "children", allow_duplicate=True),
Input("btn-py", "n_clicks"),
prevent_initial_call=True,
)
@app.callback(
# Output("output", "children"),
Output("output", "children", allow_duplicate=True),
Input("btn-py", "n_clicks"),
prevent_initial_call=True,
)
def py_callback(n_clicks) -> str:
time.sleep(3)
return f'Python called {n_clicks} times'
if __name__ == "__main__":
app.run_server(
debug=True,
)
To make it work without error, as I would argue the expected behavior should be, uncomment one of the ones without allow_duplicate
and comment out the one with allow_duplicate
in the same callback.
Example of the error message, which is a bit misleading in order to make it work, since to make it work you must actually turn it off on one of them.
Duplicate callback outputs
In the callback for output(s):
output.children@6bffe2e30aca9ad92bc2624962a39adfeb1183a1601f8386314e482125bd4027
Output 0 (output.children@6bffe2e30aca9ad92bc2624962a39adfeb1183a1601f8386314e482125bd4027) is already in use.
To resolve this, set `allow_duplicate=True` on
duplicate outputs, or combine the outputs into
one callback function, distinguishing the trigger
by using `dash.callback_context` if necessary.
"Real world" example signature, sanitized from a production application, this works (and vice versa for where it is set):
@app.callback(
Output("rerun-timer", "disabled"),
Output("output", "children"),
Output("rerun-btn", "children"),
Output("popup", "data"),
Input("rerun-btn", "n_clicks"),
State("request", "data"),
)
def onPress...
app.clientside_callback(
ClientsideFunction(namespace="page1", function_name="onPress"),
Output("output", "children"),
Output("rerun-btn", "children"),
Input("rerun-btn", "n_clicks"),
)
Additional thoughts
This is just a quick example to illustrate the issue. I understand there are arguments to be made for "combining" callbacks, but in my opinion that only applies to within the same "namespace" (python vs javascript). I also understand there is other functionality built in to callbacks such as loading_state
and running=
. At least for my Dash projects over the years (personal and professional), I have found many situations where it is beneficial to be allowed this type of combination, even while leveraging all the other features. This allows maximizing performance of the application if certain actions can be placed entirely in Javascript, and others in Python.
For a while, even before native duplicates were allowed in Dash, I have been patching dash.Dash
to allow a duplicate_output_group
argument while creating the callback IDs to allow full control over duplicates via Python vs Javascript namespaces. Basically it just attaches either -py
or -js
to the end of the callback IDs. I was going to submit this namespace logic upstream a blue moon or two ago, but never managed to get around to it before the native duplicate logic was built in. I am hoping to leverage the native allow_duplicate
logic of Dash moving forward, so that I no longer have to patch the ID creation in applications.
If it is decided this should not be allowed, then I can continue patching, but it would be nice to have full control over this behavior natively in the applications I maintain. If that is the choice, then I would ask that the error needs tweaking. Additionally, I could explain more about the namespace logic I use currently in dash.Dash
, if there is interest in supporting this in a different way (matching duplicates allowed via namespaces).