252 lines
8.5 KiB
Python
252 lines
8.5 KiB
Python
import functools
|
||
import warnings
|
||
import json
|
||
import contextvars
|
||
|
||
import flask
|
||
|
||
from . import exceptions
|
||
from ._utils import AttributeDict
|
||
|
||
|
||
context_value = contextvars.ContextVar("callback_context")
|
||
context_value.set({})
|
||
|
||
|
||
def has_context(func):
|
||
@functools.wraps(func)
|
||
def assert_context(*args, **kwargs):
|
||
if not context_value.get():
|
||
raise exceptions.MissingCallbackContextException(
|
||
f"dash.callback_context.{getattr(func, '__name__')} is only available from a callback!"
|
||
)
|
||
return func(*args, **kwargs)
|
||
|
||
return assert_context
|
||
|
||
|
||
def _get_context_value():
|
||
return context_value.get()
|
||
|
||
|
||
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}])
|
||
|
||
|
||
# pylint: disable=no-init
|
||
class CallbackContext:
|
||
@property
|
||
@has_context
|
||
def inputs(self):
|
||
return getattr(_get_context_value(), "input_values", {})
|
||
|
||
@property
|
||
@has_context
|
||
def states(self):
|
||
return getattr(_get_context_value(), "state_values", {})
|
||
|
||
@property
|
||
@has_context
|
||
def triggered(self):
|
||
"""
|
||
Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the
|
||
callback is called on initial load, unless an Input prop got its value from another initial callback.
|
||
Callbacks triggered by user actions typically have one item in triggered, unless the same action changes
|
||
two props at once or the callback has several Input props that are all modified by another callback based on
|
||
a single user action.
|
||
|
||
Example: To get the id of the component that triggered the callback:
|
||
`component_id = ctx.triggered[0]['prop_id'].split('.')[0]`
|
||
|
||
Example: To detect initial call, empty triggered is not really empty, it's falsy so that you can use:
|
||
`if ctx.triggered:`
|
||
"""
|
||
# For backward compatibility: previously `triggered` always had a
|
||
# value - to avoid breaking existing apps, add a dummy item but
|
||
# make the list still look falsy. So `if ctx.triggered` will make it
|
||
# look empty, but you can still do `triggered[0]["prop_id"].split(".")`
|
||
return getattr(_get_context_value(), "triggered_inputs", []) or falsy_triggered
|
||
|
||
@property
|
||
@has_context
|
||
def triggered_prop_ids(self):
|
||
"""
|
||
Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when
|
||
the callback is called on initial load, unless an Input prop got its value from another initial callback.
|
||
Callbacks triggered by user actions typically have one item in triggered, unless the same action changes
|
||
two props at once or the callback has several Input props that are all modified by another callback based
|
||
on a single user action.
|
||
|
||
triggered_prop_ids (dict):
|
||
- keys (str) : the triggered "prop_id" composed of "component_id.component_property"
|
||
- values (str or dict): the id of the component that triggered the callback. Will be the dict id for pattern matching callbacks
|
||
|
||
Example - regular callback
|
||
{"btn-1.n_clicks": "btn-1"}
|
||
|
||
Example - pattern matching callbacks:
|
||
{'{"index":0,"type":"filter-dropdown"}.value': {"index":0,"type":"filter-dropdown"}}
|
||
|
||
Example usage:
|
||
`if "btn-1.n_clicks" in ctx.triggered_prop_ids:
|
||
do_something()`
|
||
"""
|
||
triggered = getattr(_get_context_value(), "triggered_inputs", [])
|
||
ids = AttributeDict({})
|
||
for item in triggered:
|
||
component_id, _, _ = item["prop_id"].rpartition(".")
|
||
ids[item["prop_id"]] = component_id
|
||
if component_id.startswith("{"):
|
||
ids[item["prop_id"]] = AttributeDict(json.loads(component_id))
|
||
return ids
|
||
|
||
@property
|
||
@has_context
|
||
def triggered_id(self):
|
||
"""
|
||
Returns the component id (str or dict) of the Input component that triggered the callback.
|
||
|
||
Note - use `triggered_prop_ids` if you need both the component id and the prop that triggered the callback or if
|
||
multiple Inputs triggered the callback.
|
||
|
||
Example usage:
|
||
`if "btn-1" == ctx.triggered_id:
|
||
do_something()`
|
||
|
||
"""
|
||
component_id = None
|
||
if self.triggered:
|
||
prop_id = self.triggered_prop_ids.first()
|
||
component_id = self.triggered_prop_ids[prop_id]
|
||
return component_id
|
||
|
||
@property
|
||
@has_context
|
||
def args_grouping(self):
|
||
"""
|
||
args_grouping is a dict of the inputs used with flexible callback signatures. The keys are the variable names
|
||
and the values are dictionaries containing:
|
||
- “id”: (string or dict) the component id. If it’s a pattern matching id, it will be a dict.
|
||
- “id_str”: (str) for pattern matching ids, it’s the stringified dict id with no white spaces.
|
||
- “property”: (str) The component property used in the callback.
|
||
- “value”: the value of the component property at the time the callback was fired.
|
||
- “triggered”: (bool)Whether this input triggered the callback.
|
||
|
||
Example usage:
|
||
@app.callback(
|
||
Output("container", "children"),
|
||
inputs=dict(btn1=Input("btn-1", "n_clicks"), btn2=Input("btn-2", "n_clicks")),
|
||
)
|
||
def display(btn1, btn2):
|
||
c = ctx.args_grouping
|
||
if c.btn1.triggered:
|
||
return f"Button 1 clicked {btn1} times"
|
||
elif c.btn2.triggered:
|
||
return f"Button 2 clicked {btn2} times"
|
||
else:
|
||
return "No clicks yet"
|
||
|
||
"""
|
||
return getattr(_get_context_value(), "args_grouping", [])
|
||
|
||
@property
|
||
@has_context
|
||
def outputs_grouping(self):
|
||
return getattr(_get_context_value(), "outputs_grouping", [])
|
||
|
||
@property
|
||
@has_context
|
||
def outputs_list(self):
|
||
if self.using_outputs_grouping:
|
||
warnings.warn(
|
||
"outputs_list is deprecated, use outputs_grouping instead",
|
||
DeprecationWarning,
|
||
)
|
||
|
||
return getattr(_get_context_value(), "outputs_list", [])
|
||
|
||
@property
|
||
@has_context
|
||
def inputs_list(self):
|
||
if self.using_args_grouping:
|
||
warnings.warn(
|
||
"inputs_list is deprecated, use args_grouping instead",
|
||
DeprecationWarning,
|
||
)
|
||
|
||
return getattr(_get_context_value(), "inputs_list", [])
|
||
|
||
@property
|
||
@has_context
|
||
def states_list(self):
|
||
if self.using_args_grouping:
|
||
warnings.warn(
|
||
"states_list is deprecated, use args_grouping instead",
|
||
DeprecationWarning,
|
||
)
|
||
return getattr(_get_context_value(), "states_list", [])
|
||
|
||
@property
|
||
@has_context
|
||
def response(self):
|
||
return getattr(_get_context_value(), "dash_response")
|
||
|
||
@staticmethod
|
||
@has_context
|
||
def record_timing(name, duration=None, description=None):
|
||
"""Records timing information for a server resource.
|
||
|
||
:param name: The name of the resource.
|
||
:type name: string
|
||
|
||
:param duration: The time in seconds to report. Internally, this
|
||
is rounded to the nearest millisecond.
|
||
:type duration: float or None
|
||
|
||
:param description: A description of the resource.
|
||
:type description: string or None
|
||
"""
|
||
timing_information = getattr(flask.g, "timing_information", {})
|
||
|
||
if name in timing_information:
|
||
raise KeyError(f'Duplicate resource name "{name}" found.')
|
||
|
||
timing_information[name] = {"dur": round(duration * 1000), "desc": description}
|
||
|
||
setattr(flask.g, "timing_information", timing_information)
|
||
|
||
@property
|
||
@has_context
|
||
def using_args_grouping(self):
|
||
"""
|
||
Return True if this callback is using dictionary or nested groupings for
|
||
Input/State dependencies, or if Input and State dependencies are interleaved
|
||
"""
|
||
return getattr(_get_context_value(), "using_args_grouping", [])
|
||
|
||
@property
|
||
@has_context
|
||
def using_outputs_grouping(self):
|
||
"""
|
||
Return True if this callback is using dictionary or nested groupings for
|
||
Output dependencies.
|
||
"""
|
||
return getattr(_get_context_value(), "using_outputs_grouping", [])
|
||
|
||
@property
|
||
@has_context
|
||
def timing_information(self):
|
||
return getattr(flask.g, "timing_information", {})
|
||
|
||
|
||
callback_context = CallbackContext()
|