Skip to content

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

Open

Description

@sdementen

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}].

Activity

  1. sdementen commented on Jan 12, 2021

    @sdementen
    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)

  2. alexcjohnson commented on Jan 12, 2021

    @alexcjohnson
    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}])

  3. alexcjohnson commented on Jan 12, 2021

    @alexcjohnson
    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) ?

  4. sdementen commented on Jan 13, 2021

    @sdementen
    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

  5. alexcjohnson commented on Jan 13, 2021

    @alexcjohnson
    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.

  6. jdamiba commented on Jan 18, 2021

    @jdamiba

    @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 :)

  7. sdementen commented on Jan 18, 2021

    @sdementen
    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.

  8. alexcjohnson commented on Jan 19, 2021

    @alexcjohnson
    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!

  9. sdementen commented on Jan 23, 2021

    @sdementen
    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), ...

  10. self-assigned this
    on Jul 18, 2024
  11. removed their assignment
    on Aug 2, 2024
  12. 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}][/+] on Aug 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3backlogbugsomething broken

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions