Handle large number of Input/Output States in Dash callback using python-box

Just wanted to share something I wrote based on an older thread that I thought was fun…

Often times we have a huge callback function that becomes difficult to read and the input and output arguments make the function signature huge or requires the use of args which is then ambiguous in the function code.

This thread is very helpful: How to elegantly handle a very large number of Input/State in callbacks? - #10 by Tobs

However, I think I’ve improved on the answer with the following custom decorator using python-box:


from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Tuple

from box import Box
from dash_extensions.enrich import Input, Output, State

class Handlers:
    @staticmethod
    def _as_list(item):
        if item is None:
            return []
        if isinstance(item, tuple):
            return list(item)
        if isinstance(item, list):
            return item
        return [item]

    @staticmethod
    def _callback_inputs(*args: Any):
        outputs = []
        inputs = []
        states = []
        for arg in args:
            elements = Handlers._as_list(arg)
            for element in elements:
                if isinstance(element, Output):
                    outputs.append(element)
                elif isinstance(element, Input):
                    inputs.append(element)
                elif isinstance(element, State):
                    states.append(element)

        return outputs, inputs, states

    @staticmethod
    def callback(*args: Any, prefix=None, **kwargs: Any) -> Callable[[Any], Any]:
        """Register callback on Dash application.

        Normally used as a decorator, `@Handlers.callback` provides a server-side
        callback relating the values of one or more `Output` items to one or
        more `Input` items which will trigger the callback when they change,
        and optionally `State` items which provide additional information but
        do not trigger the callback directly.

        This function wraps the regular Dash callback function but allows the target
        function to reference arguments using the python-box.

        The optional argument `prefix` causes the callback to register values in
        python-box without a specific prefix e.g the id prefix-example would be
        accessible using `c.example` if `prefix="prefix"` was input into the callback.

        Args:
            *args[List]: Outputs, Inputs, States of callback
            **kwargs[Dict]: All key word arguments used in regular dash callback
            prefix[str]: Prefix to eliminate in python-box registration.
        """
        outputs, inputs, states = Handlers._callback_inputs(*args)

        def accept_func(func: Callable[[Any], Any]):
            @app.callback(outputs, inputs, states, **kwargs)
            @wraps(func)
            def wrapper(*args: Any):
                d = {
                    element.component_id
                    if prefix is None
                    else element.component_id.replace(prefix, ""): {
                        element.component_property: args[i],
                    }
                    for i, element in enumerate(inputs + states)
                }

                b = Box(d)

                return func(b)  # type: ignore

        return accept_func

Now you can define callbacks with a single input parameter, call it c for a component box with . syntax where the inputs and states can be accessed using c.component_id.component_property e.g c.my_button.nclicks

Here is a below example from my code:

@Handlers.callback(
    Output(id("main-view-solutions-modal"), "is_open"),
    Input(id("main-view-solutions-button"), "n_clicks"),
    Input(id("main-view-solutions-modal-close"), "n_clicks"),
    State(id("main-view-solutions-modal"), "is_open"),
    prefix=id("main-view-solutions-"),
    prevent_initial_call=True,
)
def modal_callback(c):
    if c.button.n_clicks or c.modal_close.n_clicks:
        return not c.modal.is_open
    return c.modal.is_open

Of course this is a simple example just to show but it would work the same with 20 inputs/states.

6 Likes

An ingenious way of writing the callback. I think this would be appealing to many community members.

And this is your first post, @sawyeracosty . Where have you been hiding? :smile:

Welcome to the Dash community.

1 Like

Thanks I appreciate it, just haven’t had any content to post until now but I love Dash its fun to integrate into DS projects

1 Like

I had to check that Box! python-box · PyPI

Advanced Python dictionaries with dot notation access

3 Likes