wg-backend-django/dell-env/lib/python3.11/site-packages/dash/dependencies.py
2023-10-30 14:40:43 +07:00

374 lines
13 KiB
Python

import json
from dash.development.base_component import Component
from ._validate import validate_callback
from ._grouping import flatten_grouping, make_grouping_by_index
class _Wildcard: # pylint: disable=too-few-public-methods
def __init__(self, name):
self._name = name
def __str__(self):
return self._name
def __repr__(self):
return f"<{self}>"
def to_json(self):
# used in serializing wildcards - arrays are not allowed as
# id values, so make the wildcards look like length-1 arrays.
return f'["{self._name}"]'
MATCH = _Wildcard("MATCH")
ALL = _Wildcard("ALL")
ALLSMALLER = _Wildcard("ALLSMALLER")
class DashDependency: # pylint: disable=too-few-public-methods
def __init__(self, component_id, component_property):
if isinstance(component_id, Component):
self.component_id = component_id._set_random_id()
else:
self.component_id = component_id
self.component_property = component_property
self.allow_duplicate = False
def __str__(self):
return f"{self.component_id_str()}.{self.component_property}"
def __repr__(self):
return f"<{self.__class__.__name__} `{self}`>"
def component_id_str(self):
i = self.component_id
def _dump(v):
return json.dumps(v, sort_keys=True, separators=(",", ":"))
def _json(k, v):
vstr = v.to_json() if hasattr(v, "to_json") else json.dumps(v)
return f"{json.dumps(k)}:{vstr}"
if isinstance(i, dict):
return "{" + ",".join(_json(k, i[k]) for k in sorted(i)) + "}"
return i
def to_dict(self):
return {"id": self.component_id_str(), "property": self.component_property}
def __eq__(self, other):
"""
We use "==" to denote two deps that refer to the same prop on
the same component. In the case of wildcard deps, this means
the same prop on *at least one* of the same components.
"""
return (
isinstance(other, DashDependency)
and self.component_property == other.component_property
and self._id_matches(other)
)
def _id_matches(self, other):
my_id = self.component_id
other_id = other.component_id
self_dict = isinstance(my_id, dict)
other_dict = isinstance(other_id, dict)
if self_dict != other_dict:
return False
if self_dict:
if set(my_id.keys()) != set(other_id.keys()):
return False
for k, v in my_id.items():
other_v = other_id[k]
if v == other_v:
continue
v_wild = isinstance(v, _Wildcard)
other_wild = isinstance(other_v, _Wildcard)
if v_wild or other_wild:
if not (v_wild and other_wild):
continue # one wild, one not
if v is ALL or other_v is ALL:
continue # either ALL
if v is MATCH or other_v is MATCH:
return False # one MATCH, one ALLSMALLER
else:
return False
return True
# both strings
return my_id == other_id
def __hash__(self):
return hash(str(self))
def has_wildcard(self):
"""
Return true if id contains a wildcard (MATCH, ALL, or ALLSMALLER)
"""
if isinstance(self.component_id, dict):
for v in self.component_id.values():
if isinstance(v, _Wildcard):
return True
return False
class Output(DashDependency): # pylint: disable=too-few-public-methods
"""Output of a callback."""
allowed_wildcards = (MATCH, ALL)
def __init__(self, component_id, component_property, allow_duplicate=False):
super().__init__(component_id, component_property)
self.allow_duplicate = allow_duplicate
class Input(DashDependency): # pylint: disable=too-few-public-methods
"""Input of callback: trigger an update when it is updated."""
allowed_wildcards = (MATCH, ALL, ALLSMALLER)
class State(DashDependency): # pylint: disable=too-few-public-methods
"""Use the value of a State in a callback but don't trigger updates."""
allowed_wildcards = (MATCH, ALL, ALLSMALLER)
class ClientsideFunction: # pylint: disable=too-few-public-methods
def __init__(self, namespace=None, function_name=None):
if namespace.startswith("_dashprivate_"):
raise ValueError("Namespaces cannot start with '_dashprivate_'.")
if namespace in ["PreventUpdate", "no_update"]:
raise ValueError(
f'"{namespace}" is a forbidden namespace in dash_clientside.'
)
self.namespace = namespace
self.function_name = function_name
def __repr__(self):
return f"ClientsideFunction({self.namespace}, {self.function_name})"
def extract_grouped_output_callback_args(args, kwargs):
if "output" in kwargs:
parameters = kwargs["output"]
# Normalize list/tuple of multiple positional outputs to a tuple
if isinstance(parameters, (list, tuple)):
parameters = list(parameters)
# Make sure dependency grouping contains only Output objects
for dep in flatten_grouping(parameters):
if not isinstance(dep, Output):
raise ValueError(
f"Invalid value provided where an Output dependency "
f"object was expected: {dep}"
)
return parameters
parameters = []
while args:
next_deps = flatten_grouping(args[0])
if all(isinstance(d, Output) for d in next_deps):
parameters.append(args.pop(0))
else:
break
return parameters
def extract_grouped_input_state_callback_args_from_kwargs(kwargs):
input_parameters = kwargs["inputs"]
if isinstance(input_parameters, DashDependency):
input_parameters = [input_parameters]
state_parameters = kwargs.get("state", None)
if isinstance(state_parameters, DashDependency):
state_parameters = [state_parameters]
if isinstance(input_parameters, dict):
# Wrapped function will be called with named keyword arguments
if state_parameters:
if not isinstance(state_parameters, dict):
raise ValueError(
"The input argument to app.callback was a dict, "
"but the state argument was not.\n"
"input and state arguments must have the same type"
)
# Merge into state dependencies
parameters = state_parameters
parameters.update(input_parameters)
else:
parameters = input_parameters
return parameters
if isinstance(input_parameters, (list, tuple)):
# Wrapped function will be called with positional arguments
parameters = list(input_parameters)
if state_parameters:
if not isinstance(state_parameters, (list, tuple)):
raise ValueError(
"The input argument to app.callback was a list, "
"but the state argument was not.\n"
"input and state arguments must have the same type"
)
parameters += list(state_parameters)
return parameters
raise ValueError(
"The input argument to app.callback may be a dict, list, or tuple,\n"
f"but received value of type {type(input_parameters)}"
)
def extract_grouped_input_state_callback_args_from_args(args):
# Collect input and state from args
parameters = []
while args:
next_deps = flatten_grouping(args[0])
if all(isinstance(d, (Input, State)) for d in next_deps):
parameters.append(args.pop(0))
else:
break
if len(parameters) == 1:
# Only one output grouping, return as-is
return parameters[0]
# Multiple output groupings, return wrap in tuple
return parameters
def extract_grouped_input_state_callback_args(args, kwargs):
if "inputs" in kwargs:
return extract_grouped_input_state_callback_args_from_kwargs(kwargs)
if "state" in kwargs:
# Not valid to provide state as kwarg without input as kwarg
raise ValueError(
"The state keyword argument may not be provided without "
"the input keyword argument"
)
return extract_grouped_input_state_callback_args_from_args(args)
def compute_input_state_grouping_indices(input_state_grouping):
# Flatten grouping of Input and State dependencies into a flat list
flat_deps = flatten_grouping(input_state_grouping)
# Split into separate flat lists of Input and State dependencies
flat_inputs = [dep for dep in flat_deps if isinstance(dep, Input)]
flat_state = [dep for dep in flat_deps if isinstance(dep, State)]
# For each entry in the grouping, compute the index into the
# concatenation of flat_inputs and flat_state
total_inputs = len(flat_inputs)
input_count = 0
state_count = 0
flat_inds = []
for dep in flat_deps:
if isinstance(dep, Input):
flat_inds.append(input_count)
input_count += 1
else:
flat_inds.append(total_inputs + state_count)
state_count += 1
# Reshape this flat list of indices to match the input grouping
grouping_inds = make_grouping_by_index(input_state_grouping, flat_inds)
return flat_inputs, flat_state, grouping_inds
def handle_grouped_callback_args(args, kwargs):
"""Split args into outputs, inputs and states"""
prevent_initial_call = kwargs.get("prevent_initial_call", None)
if prevent_initial_call is None and args and isinstance(args[-1], bool):
args, prevent_initial_call = args[:-1], args[-1]
# flatten args, to support the older syntax where outputs, inputs, and states
# each needed to be in their own list
flat_args = []
for arg in args:
flat_args += arg if isinstance(arg, (list, tuple)) else [arg]
outputs = extract_grouped_output_callback_args(flat_args, kwargs)
flat_outputs = flatten_grouping(outputs)
if isinstance(outputs, (list, tuple)) and len(outputs) == 1:
out0 = kwargs.get("output", args[0] if args else None)
if not isinstance(out0, (list, tuple)):
# unless it was explicitly provided as a list, a single output
# should be unwrapped. That ensures the return value of the
# callback is also not expected to be wrapped in a list.
outputs = outputs[0]
inputs_state = extract_grouped_input_state_callback_args(flat_args, kwargs)
flat_inputs, flat_state, input_state_indices = compute_input_state_grouping_indices(
inputs_state
)
types = Input, Output, State
validate_callback(flat_outputs, flat_inputs, flat_state, flat_args, types)
return outputs, flat_inputs, flat_state, input_state_indices, prevent_initial_call
def extract_callback_args(args, kwargs, name, type_):
"""Extract arguments for callback from a name and type"""
parameters = kwargs.get(name, [])
if parameters:
if not isinstance(parameters, (list, tuple)):
# accept a single item, not wrapped in a list, for any of the
# categories as a named arg (even though previously only output
# could be given unwrapped)
return [parameters]
else:
while args and isinstance(args[0], type_):
parameters.append(args.pop(0))
return parameters
def handle_callback_args(args, kwargs):
"""Split args into outputs, inputs and states"""
prevent_initial_call = kwargs.get("prevent_initial_call", None)
if prevent_initial_call is None and args and isinstance(args[-1], bool):
args, prevent_initial_call = args[:-1], args[-1]
# flatten args, to support the older syntax where outputs, inputs, and states
# each needed to be in their own list
flat_args = []
for arg in args:
flat_args += arg if isinstance(arg, (list, tuple)) else [arg]
outputs = extract_callback_args(flat_args, kwargs, "output", Output)
validate_outputs = outputs
if len(outputs) == 1:
out0 = kwargs.get("output", args[0] if args else None)
if not isinstance(out0, (list, tuple)):
# unless it was explicitly provided as a list, a single output
# should be unwrapped. That ensures the return value of the
# callback is also not expected to be wrapped in a list.
outputs = outputs[0]
inputs = extract_callback_args(flat_args, kwargs, "inputs", Input)
states = extract_callback_args(flat_args, kwargs, "state", State)
types = Input, Output, State
validate_callback(validate_outputs, inputs, states, flat_args, types)
return outputs, inputs, states, prevent_initial_call