Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] #1523

Open
sdementen opened this issue Jan 12, 2021 · 9 comments
Labels
bug something broken P3 backlog

Comments

@sdementen
Copy link

Describe your context

I am trying to use a Store that could be written by multiple callbacks (to circumvent the 1 output can only be updated by one callback). The logic (hack?) looks sound and works except that I get a problem with a callback that is triggered with
dash.callback_contract.triggered == [{'prop_id': '.', 'value': None}] each time an unrelated Div is updated (ie not only on the first firing at the beginning of the app) and is moreover triggered twice.

Here is the sample app with the get_multi_store function generating a Div with multiple stores (one core and one per writer).

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

app = dash.Dash()


def get_multi_store(id_store, id_writers, mode="replace"):
    """Generate a store that can be written by multiple writers.

    For input, use Input(id_store, "data").
    For output by writer 'x', use Output(id_store+"-x", "data").

    If mode=="replace", the data of the store is replaced when written to it.
    If mode=="append", the data of the store is appended with the new data.

    """
    assert mode in {"replace", "append"}
    N = len(id_writers)

    id_container = f"{id_store}-container"
    id_store_root = id_store
    id_store_writers = [f"{id_store}-{id_writer}" for id_writer in id_writers]


    def get_layout(data):
        """Generate the layout with the root read-only store and the writable stores"""
        return [dcc.Store(id=id_store_root, data=data)] + [
            dcc.Store(id=id_store_writer) for id_store_writer in id_store_writers
        ]

    @app.callback(
        Output(id_container, "children"),
        [Input(id_store_writer, "modified_timestamp") for id_store_writer in id_store_writers],
        [State(id_store_writer, "data") for id_store_writer in id_store_writers]
        + [State(id_store_root, "data")],
    )
    def update_stores(*args):
        """Callback to handle the update of the root store based on the writer stores"""
        # process args to retrieve the different input/state components
        # extract data from root store
        *args, data_root = args
        # reorganise stores data as [(ts1, data1), ...]
        stores_info = list(zip(args[:N], args[N:]))
        # keep stores with timestamp defined and sort them by the timestamp
        stores_info = sorted(
            ((ts, data) for (ts, data) in stores_info if ts is not None), key=lambda item: item[0]
        )
        # print(stores_info)
        # print(dash.callback_context.triggered)
        if not stores_info:
            # no store updated
            return dash.no_update

        # in function of mode, update the data_root
        if mode == "replace":
            # replace the data_root by the more recent data store write
            ts, new_data = stores_info[-1]
            assert ts
            if new_data == data_root:
                return dash.no_update
            data_root = new_data

        else:
            # append to data_root all data store writes (chronologically)
            data_root.extend(data for ts, data in stores_info if ts)

        # rerender the layout with the new data
        layout = get_layout(data_root)

        return layout

    initial_data = None if mode == "replace" else []
    layout = html.Div(id=id_container, children=get_layout(initial_data))

    return layout


app.layout = html.Div(
    [
        dcc.Input(id="a", value="0", type="number"),
        dcc.Input(id="b", value="0", type="number"),
        dcc.Input(id="c", value="0", type="number"),
        html.Div(id="output-store"),
        get_multi_store("scenario", ["a", "b", "c"], mode="append"),
    ]
)


# display output store (for debugging purposes)
@app.callback(
    Output(component_id="output-store", component_property="children"),
    [Input(component_id="scenario", component_property="data")],
)
def display_store(c):
    return str(c)


# update a with b
@app.callback(
    Output(component_id="scenario-c", component_property="data"),
    [Input(component_id="c", component_property="value")],
)
def set_c(c):
    return {"c": c}


# update store with b
@app.callback(
    Output(component_id="scenario-b", component_property="data"),
    [Input(component_id="b", component_property="value")],
)
def set_b(b):
    return {"b": b}


# update store with a and update c (which triggers and update of store with c)
@app.callback(
    [
        Output(component_id="c", component_property="value"),
        Output(component_id="scenario-a", component_property="data"),
    ],
    [Input(component_id="a", component_property="value")],
)
def set_a_and_c(a):
    print(dash.callback_context.triggered)
    # prints [{'prop_id': '.', 'value': None}]
    return float(a) / 2, {"a": a}


if __name__ == "__main__":
    app.run_server(debug=True)
  • replace the result of pip list | grep dash below
dash                      1.18.1
dash-core-components      1.14.1
dash-html-components      1.1.1
dash-renderer             1.8.3
  • if frontend related, tell us your Browser, Version and OS

    • OS: windows 10
    • Browser chrome
    • Version: 87

Describe the bug

After the initialization of the app, the Store "scenario" holds the value:
[{'a': '0'}, {'b': '0'}, {'c': 0}]
When increasing the Input "a", the store changes to (as the input 'a' and the input 'c' are successively changed)
[{'a': '0'}, {'b': '0'}, {'c': 0}, {'a': 1}, {'c': 0.5}]
However, I see that the callback set_a_and_c is triggered once when the input 'a' is changed (OK) and twice afterwards with the line print(dash.callback_context.triggered) printing [{'prop_id': '.', 'value': None}]

Expected behavior

I would expect the callback set_a_and_c to be only triggered once with dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].
Moreover, if I change the input 'c', I also see that the callback set_a_and_c is called twice with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

If I update the input 'b', then the callback set_a_and_c is called once with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

@sdementen
Copy link
Author

To circumvent the issue, I could use the following decorator

def avoid_untriggered_call(f):
    """Decorator that cancel any call to the callback if the trigger is [{"prop_id": ".", "value": None}]."""

    @wraps(f)
    def helper(*args, **kwargs):
        if dash.callback_context.triggered == [{"prop_id": ".", "value": None}]:
            return dash.no_update
        return f(*args, **kwargs)

    return helper

on each of the callback but then I would miss the initial calls to the callback at the start of the app (AFAIK)

@alexcjohnson
Copy link
Collaborator

Thanks @sdementen - possibly related to #1519

The callback_context you're seeing is meant for the initial call of a callback where in most cases there is (according to the convention we adopted) no trigger for the callback. This value is there for backward compatibility with older versions of Dash that always reported exactly one triggering input. It doesn't make sense that this behavior is happening in this case though, so this is definitely a bug.

While we're here I'll point out that instead of if dash.callback_context.triggered == [{"prop_id": ".", "value": None}] you can say if not dash.callback_context.triggered because of the way we constructed it. That's a better pattern to use for new code because in a future major version of Dash we may take out this weird value and just use []

class FalsyList(list):
def __bool__(self):
# for Python 3
return False
def __nonzero__(self):
# for Python 2
return False
falsy_triggered = FalsyList([{"prop_id": ".", "value": None}])

@alexcjohnson alexcjohnson added the bug something broken label Jan 12, 2021
@alexcjohnson
Copy link
Collaborator

Oh actually, I didn't fully understand what you were doing at first. You've found the loophole that effectively you can create a circular callback chain by having the output of a callback be its parent's children prop. That means you're recreating the store components, thus their callbacks are triggered in a way that looks like an initial callback. I'm not sure why this is happening twice, but I don't think there's a lot we can do to avoid strange behavior in this case - if the values you create this way don't converge it's possible you'd trigger an infinite loop of callbacks.

I'm not clear on why you need this to function like multiple callbacks all outputting to the same prop, as opposed to having all of those same inputs feeding into one callback, or at least having a separate callback combining them all into one final output. What about a clientside merge callback, like @nicolaskruchten describes in plotly/dash-core-components#881 (comment) ?

@alexcjohnson alexcjohnson removed the bug something broken label Jan 12, 2021
@sdementen
Copy link
Author

tx @alexcjohnson for your insight!

On my initial problem, I am indeed changing the parent container children so that it resets the writer stores. Hence, at any time, only the root store has content (as the writer stores have been reset) except when a callback is triggered by one of the writer stores. The writer stores are only used as
What I do not understand is that when I change the parent container, only the callbacks with the Stores in "input" should be triggered, right (but I have a doubt now...) ? and in my case, the Stores are only used as outputs. I would expect that, once the parent container is updated, no callback triggered except the one using the root Store as Input.
If this is not the case and that callbacks are triggered whenever an Input but also an Output element is created, then you can close the issue.
The documentation in https://dash.plotly.com/advanced-callbacks states that only callbacks which have as input the new component will be triggered

When Dash Components Are Added To The Layout
It is possible for a callback to insert new Dash components into a Dash app's layout. If these new components are themselves the inputs to other callback functions, then their appearance in the Dash app's layout will trigger those callback functions to be executed.

In this circumstance, it is possible that multiple requests are made to execute the same callback function. This would occur if the callback in question has already been requested and its output returned before the new components which are also its inputs are added to the layout.

Regarding the client side callback, this is indeed an excellent idea/suggestion. It removes all "spurious" callbacks and make the whole more reactive. I will make a post on the plotly community when the details are ironed out

@alexcjohnson
Copy link
Collaborator

Ah yes, that part of the advanced callbacks doc page is not quite correct.

If the new components added to the layout contain callback outputs, these callbacks also need to fire. This is similar to what happens on page load: we don't assume the component was created with the prop values it ultimately should have, we allow the callback to run and decide that.

There is some subtle logic in this case surrounding what props count as having triggered the callback - the key distinction being whether the entire callback (inputs and outputs at least, I'd have to look back to see where we landed on state) is contained within the new layout fragment. If so, it's treated EXACTLY like the initial page load with respect to callback_context.triggered ie none of the inputs appear as triggers unless there's a chain and an input is itself an output of another callback. If any of the inputs/outputs is outside the new layout fragment, then the inputs within that fragment DO count as triggers. The initial discussion of this was here in the pattern-matching callbacks PR: #1103 (comment)

cc @jdamiba - I missed this point when we were discussing these details, perhaps we can elaborate on this section? Most important is the point about newly-created outputs triggering callbacks, but we should sort out the details re: triggers and document this as well.

@jdamiba
Copy link

jdamiba commented Jan 18, 2021

@alexcjohnson I agree that it would be a good idea to update the section that covers this behavior in dash-docs. Send me an invite for a meeting so we can get on the same page :)

@sdementen
Copy link
Author

@jdamiba @alexcjohnson it would be nice, besides the update of this section of the doc, to have a documentation on the interactions between frontend/client and server with the logic around the callback graph, the format of the JSON send from the client to the server and vice-versa.
I am using django-plotly-dash (and sending some PR there) and it involves some reverse engineering to understand the protocol and assumptions used in Dash.

@alexcjohnson
Copy link
Collaborator

documentation on the interactions between frontend/client and server with the logic around the callback graph, the format of the JSON send from the client to the server and vice-versa.

Logic around the callback graph definitely, we should document fully how and when each callback is triggered in many different cases. @jdamiba has added all sorts of great info about this in https://dash.plotly.com/advanced-callbacks, "When Are Callbacks Executed?" as well as https://dash.plotly.com/app-lifecycle and he and I will take another pass through this at some point, but if there's still missing or incorrect info there please let us know (or make a PR 🏆 )

And I suppose given that we have multiple back ends, the client-server contract is relatively firm. It's not real high on our priority list but we'd happily include this in the docs if anyone is motivated to write it!

@sdementen
Copy link
Author

I would be interested in the documentation of the graph of dependencies on the client side and on the logic that trigger the callbacks in function of the graph events.
And this to understand better dash and tinker about possible evolutions to allow cycles in the graph (amuet avoid infinite loops), to sync components, to add the ability to trigger conditionally (i.e. if the object observed satisfies some conditions, then trigger the callback, otherwise do not), ...

@gvwilson gvwilson self-assigned this Jul 18, 2024
@gvwilson gvwilson removed their assignment Aug 2, 2024
@gvwilson gvwilson added P3 backlog bug something broken labels Aug 13, 2024
@gvwilson gvwilson changed the title [BUG] callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] Aug 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken P3 backlog
Projects
None yet
Development

No branches or pull requests

4 participants