Skip to content

Callback map sharing #789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
27 changes: 27 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,30 @@ def create_callback_id(output):
return '{}.{}'.format(
output.component_id, output.component_property
)


class RedisDict:

def __init__(self, redis_kwargs):
import redis
import dill
self.serializer = dill
self.r = redis.Redis(**redis_kwargs)

def __iter__(self):

for k in self.r.keys():
yield k.decode('utf-8')

def __getitem__(self, k):

return self.serializer.loads(self.r.get(k))

def __setitem__(self, k, v):

self.r.set(k, self.serializer.dumps(v))

def items(self):

for k in self.r.keys():
yield k.decode('utf-8'), self[k]
242 changes: 123 additions & 119 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from . import _watch
from ._utils import get_asset_path as _get_asset_path
from ._utils import create_callback_id as _create_callback_id
from ._utils import RedisDict
from ._configs import (get_combined_config, pathname_configs)
from .version import __version__

Expand Down Expand Up @@ -224,6 +225,7 @@ def __init__(
suppress_callback_exceptions=None,
show_undo_redo=False,
plugins=None,
callback_redis=None,
**obsolete):

for key in obsolete:
Expand Down Expand Up @@ -293,7 +295,10 @@ def __init__(
)

# list of dependencies
self.callback_map = {}
if callback_redis is None:
self.callback_map = {}
else:
self.callback_map = RedisDict(callback_redis)

# index_string has special setter so can't go in config
self._index_string = ''
Expand Down Expand Up @@ -951,7 +956,7 @@ def duplicate_check():
else:
def duplicate_check():
return callback_id in callbacks
if duplicate_check():
if duplicate_check() and isinstance(self.callback_map, dict):
if is_multi:
msg = '''
Multi output {} contains an `Output` object
Expand All @@ -974,110 +979,6 @@ def duplicate_check():
).replace(' ', '')
raise exceptions.DuplicateCallbackOutput(msg)

@staticmethod
def _validate_callback_output(output_value, output):
valid = [str, dict, int, float, type(None), Component]

def _raise_invalid(bad_val, outer_val, path, index=None,
toplevel=False):
bad_type = type(bad_val).__name__
outer_id = "(id={:s})".format(outer_val.id) \
if getattr(outer_val, 'id', False) else ''
outer_type = type(outer_val).__name__
raise exceptions.InvalidCallbackReturnValue('''
The callback for `{output:s}`
returned a {object:s} having type `{type:s}`
which is not JSON serializable.

{location_header:s}{location:s}
and has string representation
`{bad_val}`

In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those.
'''.format(
output=repr(output),
object='tree with one value' if not toplevel else 'value',
type=bad_type,
location_header=(
'The value in question is located at'
if not toplevel else
'''The value in question is either the only value returned,
or is in the top level of the returned list,'''
),
location=(
"\n" +
("[{:d}] {:s} {:s}".format(index, outer_type, outer_id)
if index is not None
else ('[*] ' + outer_type + ' ' + outer_id))
+ "\n" + path + "\n"
) if not toplevel else '',
bad_val=bad_val
).replace(' ', ''))

def _value_is_valid(val):
return (
# pylint: disable=unused-variable
any([isinstance(val, x) for x in valid]) or
type(val).__name__ == 'unicode'
)

def _validate_value(val, index=None):
# val is a Component
if isinstance(val, Component):
# pylint: disable=protected-access
for p, j in val._traverse_with_paths():
# check each component value in the tree
if not _value_is_valid(j):
_raise_invalid(
bad_val=j,
outer_val=val,
path=p,
index=index
)

# Children that are not of type Component or
# list/tuple not returned by traverse
child = getattr(j, 'children', None)
if not isinstance(child, (tuple,
collections.MutableSequence)):
if child and not _value_is_valid(child):
_raise_invalid(
bad_val=child,
outer_val=val,
path=p + "\n" + "[*] " + type(child).__name__,
index=index
)

# Also check the child of val, as it will not be returned
child = getattr(val, 'children', None)
if not isinstance(child, (tuple, collections.MutableSequence)):
if child and not _value_is_valid(child):
_raise_invalid(
bad_val=child,
outer_val=val,
path=type(child).__name__,
index=index
)

# val is not a Component, but is at the top level of tree
else:
if not _value_is_valid(val):
_raise_invalid(
bad_val=val,
outer_val=type(val).__name__,
path='',
index=index,
toplevel=True
)

if isinstance(output_value, list):
for i, val in enumerate(output_value):
_validate_value(val, index=i)
else:
_validate_value(output_value)

# pylint: disable=dangerous-default-value
def clientside_callback(
self, clientside_function, output, inputs=[], state=[]):
Expand Down Expand Up @@ -1158,17 +1059,6 @@ def callback(self, output, inputs=[], state=[]):
callback_id = _create_callback_id(output)
multi = isinstance(output, (list, tuple))

self.callback_map[callback_id] = {
'inputs': [
{'id': c.component_id, 'property': c.component_property}
for c in inputs
],
'state': [
{'id': c.component_id, 'property': c.component_property}
for c in state
],
}

def wrap_func(func):
@wraps(func)
def add_context(*args, **kwargs):
Expand Down Expand Up @@ -1227,7 +1117,7 @@ def add_context(*args, **kwargs):
cls=plotly.utils.PlotlyJSONEncoder
)
except TypeError:
self._validate_callback_output(output_value, output)
validate_callback_output(output_value, output)
raise exceptions.InvalidCallbackReturnValue('''
The callback for property `{property:s}`
of component `{id:s}` returned a value
Expand All @@ -1243,7 +1133,17 @@ def add_context(*args, **kwargs):

return jsonResponse

self.callback_map[callback_id]['callback'] = add_context
self.callback_map[callback_id] = {
'inputs': [
{'id': c.component_id, 'property': c.component_property}
for c in inputs
],
'state': [
{'id': c.component_id, 'property': c.component_property}
for c in state
],
'callback': add_context
}

return add_context

Expand Down Expand Up @@ -1692,3 +1592,107 @@ def run_server(
self.logger.info('Debugger PIN: %s', debugger_pin)

self.server.run(port=port, debug=debug, **flask_run_options)


def validate_callback_output(output_value, output):
valid = [str, dict, int, float, type(None), Component]

def _raise_invalid(bad_val, outer_val, path, index=None,
toplevel=False):
bad_type = type(bad_val).__name__
outer_id = "(id={:s})".format(outer_val.id) \
if getattr(outer_val, 'id', False) else ''
outer_type = type(outer_val).__name__
raise exceptions.InvalidCallbackReturnValue('''
The callback for `{output:s}`
returned a {object:s} having type `{type:s}`
which is not JSON serializable.

{location_header:s}{location:s}
and has string representation
`{bad_val}`

In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those.
'''.format(
output=repr(output),
object='tree with one value' if not toplevel else 'value',
type=bad_type,
location_header=(
'The value in question is located at'
if not toplevel else
'''The value in question is either the only value returned,
or is in the top level of the returned list,'''
),
location=(
"\n" +
("[{:d}] {:s} {:s}".format(index, outer_type, outer_id)
if index is not None
else ('[*] ' + outer_type + ' ' + outer_id))
+ "\n" + path + "\n"
) if not toplevel else '',
bad_val=bad_val
).replace(' ', ''))

def _value_is_valid(val):
return (
# pylint: disable=unused-variable
any([isinstance(val, x) for x in valid]) or
type(val).__name__ == 'unicode'
)

def _validate_value(val, index=None):
# val is a Component
if isinstance(val, Component):
# pylint: disable=protected-access
for p, j in val._traverse_with_paths():
# check each component value in the tree
if not _value_is_valid(j):
_raise_invalid(
bad_val=j,
outer_val=val,
path=p,
index=index
)

# Children that are not of type Component or
# list/tuple not returned by traverse
child = getattr(j, 'children', None)
if not isinstance(child, (tuple,
collections.MutableSequence)):
if child and not _value_is_valid(child):
_raise_invalid(
bad_val=child,
outer_val=val,
path=p + "\n" + "[*] " + type(child).__name__,
index=index
)

# Also check the child of val, as it will not be returned
child = getattr(val, 'children', None)
if not isinstance(child, (tuple, collections.MutableSequence)):
if child and not _value_is_valid(child):
_raise_invalid(
bad_val=child,
outer_val=val,
path=type(child).__name__,
index=index
)

# val is not a Component, but is at the top level of tree
else:
if not _value_is_valid(val):
_raise_invalid(
bad_val=val,
outer_val=type(val).__name__,
path='',
index=index,
toplevel=True
)

if isinstance(output_value, list):
for i, val in enumerate(output_value):
_validate_value(val, index=i)
else:
_validate_value(output_value)
4 changes: 3 additions & 1 deletion requires-install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ selenium
percy
requests[security]
beautifulsoup4
waitress
waitress
redis
dill
Loading