Flexible Callback Signatures

Hi everyone,

I am trying to implement tests for Dash callbacks. I am using flexible callback signature, so that I have grouped inputs to callback function. Something like code snippet below:

@callback(
    Output("container", "children"),
    inputs={
        "all_inputs": {
            "btn1": Input("btn-1", "n_clicks"),
            "btn2": Input("btn-2", "n_clicks"),
            "btn3": Input("btn-3", "n_clicks")
        }
    },
)
def display(all_inputs):
    c = ctx.args_grouping.all_inputs
    if c.btn1.triggered:
        return f"Button 1 clicked {c.btn1.value} times"
    elif c.btn2.triggered:
        return f"Button 2 clicked {c.btn2.value} times"
    elif c.btn3.triggered:
        return f"Button 3 clicked {c.btn3.value} times"

I would tried to mock/patch callback context as in example on Plotly webpage, but it didnt work:

from contextvars import copy_context
from dash._callback_context import context_value
from dash._utils import AttributeDict

# Import the names of callback functions you want to test
from app import display, update

def test_update_callback():
    output = update(1, 0)
    assert output == 'button 1: 1 & button 2: 0'

def test_display_callback():
    def run_callback():
        context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "btn-1-ctx-example.n_clicks"}]}))
        return display(1, 0, 0)

    ctx = copy_context()
    output = ctx.run(run_callback)
    assert output == f'You last clicked button with ID btn-1-ctx-example'


I was getting error saying that “all_inputs are not list element…”.

Do you have experience with writing unittests for callbacks with flexible signatures?

Best,
Milan

hi @Milan
My colleague had some thoughts here.

There seems to be a logic error in the post, the all_inputs is not used in the function and then it calls the function with multiple argument while it only takes one argument.

The original code in the docs is:

@callback(Output('container','children'),
              Input('btn-1', 'n_clicks'),
              Input('btn-2', 'n_clicks'),
              Input('btn-3', 'n_clicks'))
def display(btn1, btn2, btn3):
    button_clicked = ctx.triggered_id
    return f'You last clicked button with ID {button_clicked}'

But here it changed to use arg_groupings which uses another context var that needs mocking. The call needs to be changed to something like {'btn1': 1}

I never really used arg_grouping, so I’m not sure how it works, and I can’t recommend using it in tests since it seems to require a bunch of stuff.

I’d recommend using a dash_duo approach to test those callbacks: Dash Testing | Dash for Python Documentation | Plotly

Thanks @Adam for your message.

Yeah, that is written in documentation in example on Dash page.

But, I have example with flexible callback that I need to test. My usecase is very similar to this example on bottom of this page: Flexible Callback Signatures | Dash for Python Documentation | Plotly .

I am interested in how to test that callback that has flexible callback signature?

Best,
Milan

Actually, this callback (which uses Dash flexible callback signature):

@callback(
    Output("container", "children"),
    inputs={
        "all_inputs": {
            "btn1": Input("btn-1", "n_clicks"),
            "btn2": Input("btn-2", "n_clicks"),
            "btn3": Input("btn-3", "n_clicks")
        }
    },
)
def display(all_inputs):
    c = ctx.args_grouping.all_inputs
    if c.btn1.triggered:
        return f"Button 1 clicked {c.btn1.value} times"
    elif c.btn2.triggered:
        return f"Button 2 clicked {c.btn2.value} times"
    elif c.btn3.triggered:
        return f"Button 3 clicked {c.btn3.value} times"

Can be written this way:

@callback(Output('container','children'),
              Input('btn-1', 'n_clicks'),
              Input('btn-2', 'n_clicks'),
              Input('btn-3', 'n_clicks'))
def display(btn1, btn2, btn3):
    button_clicked = ctx.triggered_id
    return f'You last clicked button with ID {button_clicked}'

But, the thing is that at company I work for, we have a lot of legacy code that was written using Dash flexible callback, and we need to write unittests for them now. Rewriting all callbacks would be very difficult at this stage. So, I am looking for way to test Dash callbacks with flexible signatures…

Thanks,
Milan

@Milan There might have been some misunderstanding. Let me clarify.
The error is coming from:
return display(1, 0, 0)

It’s no longer valid call. You need to call like display({'input': 'input-value'})

For the context you’ll have to mock the same stuff we do here: dash/dash/dash.py at 95665785f184aba4ce462637c30ccb7789280911 · plotly/dash · GitHub

Is this helpful?

Hey @adamschroeder ,

I am trying to write unittest for this kind of callback:


from dash import Input, Output, callback, ctx, Dash, html


app = Dash()


app.layout = html.Div([

    html.Button("Btn 1", id='btn-1'),
    html.Button("Btn 1", id='btn-2'),
    html.Button("Btn 1", id='btn-3'),

    html.Div(id='container')

])


@callback(
    Output("container", "children"),
    inputs={
        "all_inputs": {
            "btn1": Input("btn-1", "n_clicks"),
            "btn2": Input("btn-2", "n_clicks"),
            "btn3": Input("btn-3", "n_clicks")
        }
    },
)
def display(all_inputs):
    c = ctx.args_grouping.all_inputs
    if c.btn1.triggered:
        return f"Button 1 clicked {c.btn1.value} times"
    elif c.btn2.triggered:
        return f"Button 2 clicked {c.btn2.value} times"
    elif c.btn3.triggered:
        return f"Button 3 clicked {c.btn3.value} times"


if __name__ == '__main__':
    app.run()


This is type of callback that we have in our Dash project. I know, it is possible to rewrite callback in a way that it does not use flexible callback signatures, but it will require big refactoring of project. Almost all callbacks in project that I work on are written using flexible callback signatures.

I am curious how could I write unittest for display() callback here? I know that callback context should be mocked, but I have no idea how to do it (because mocking should be done differently than this example shows Dash Testing | Dash for Python Documentation | Plotly . This example is for mocking callback context of simple callback, not one with flexible callback signatures).

Thanks,
Milan

Hey @AnnMarieW can you help in this thread?

Did you have experience in testing Dash flexible callback signatures?

Thanks!!!

Hi @Milan

I would just test it using dash_duo like this:

from dash import Dash, Input, Output, callback, ctx, Dash, html


def test_001bu_button_callbacks(dash_duo):
    app = Dash(__name__)

    app.layout = html.Div([

        html.Button("Btn 1", id='btn-1'),
        html.Button("Btn 1", id='btn-2'),
        html.Button("Btn 1", id='btn-3'),

        html.Div(id='container', children="")

    ])

    @callback(
        Output("container", "children"),
        inputs={
            "all_inputs": {
                "btn1": Input("btn-1", "n_clicks"),
                "btn2": Input("btn-2", "n_clicks"),
                "btn3": Input("btn-3", "n_clicks")
            }
        },
    )
    def display(all_inputs):
        c = ctx.args_grouping.all_inputs
        if c.btn1.triggered:
            return f"Button 1 clicked {c.btn1.value} times"
        elif c.btn2.triggered:
            return f"Button 2 clicked {c.btn2.value} times"
        elif c.btn3.triggered:
            return f"Button 3 clicked {c.btn3.value} times"

    dash_duo.start_server(app)

    # Wait for the app to load
    dash_duo.wait_for_text_to_equal("#container", "")

    dash_duo.find_element("#btn-1").click()
    dash_duo.wait_for_text_to_equal("#container", "Button 1 clicked 1 times")
    dash_duo.find_element("#btn-2").click()
    dash_duo.wait_for_text_to_equal("#container", "Button 2 clicked 1 times")
    dash_duo.find_element("#btn-3").click()
    dash_duo.wait_for_text_to_equal("#container", "Button 3 clicked 1 times")


    assert dash_duo.get_logs() == []


1 Like