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.