diff --git a/dash/_utils.py b/dash/_utils.py index bc71fde955..97f3cef90d 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -1,3 +1,8 @@ +import collections +from . import exceptions +from .development.base_component import Component + + def interpolate_str(template, **data): s = template for k, v in data.items(): @@ -55,3 +60,161 @@ def first(self, *names): value = self.get(name) if value: return value + + +def _raise_invalid(output, bad_val, outer_val, bad_type, path, index=None, + toplevel=False): + 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 property `{property:s}` of component `{id:s}` + returned a {object:s} having type `{type:s}` + which can not be serialized by Dash. + + {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 un-nested lists of those. + '''.format( + property=output.component_property, + id=output.component_id, + 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 _validate_callback_output(output_value, output): + valid = [str, dict, int, float, type(None), Component] + + 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): + for p, j in val.traverse_with_paths(): + # check each component value in the tree + if not _value_is_valid(j): + _raise_invalid( + output=output, + bad_val=j, + outer_val=val, + bad_type=type(j).__name__, + path=p, + index=index + ) + + # Children that are not of type Component or + # collections.MutableSequence not returned by traverse + child = getattr(j, 'children', None) + if not isinstance(child, collections.MutableSequence): + if child and not _value_is_valid(child): + _raise_invalid( + output=output, + bad_val=child, + outer_val=val, + bad_type=type(child).__name__, + 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, collections.MutableSequence): + if child and not _value_is_valid(child): + _raise_invalid( + output=output, + bad_val=child, + outer_val=val, + bad_type=type(child).__name__, + 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( + output=output, + bad_val=val, + outer_val=type(val).__name__, + bad_type=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) + + +def _validate_children_callback_output(output_value, output): + + def _is_nested_list(value): + if isinstance(value, list): + for subval in value: + if isinstance(subval, list): + return True + return False + + def _validate_value(val, index=None): + # Make sure there are no nested dicts in component tree + if isinstance(val, Component): + for p, j in val.traverse_with_paths(): + child = getattr(j, 'children', None) + if _is_nested_list(child): + _raise_invalid( + output=output, + bad_val=child, + outer_val=j, + bad_type=type(child).__name__, + path=p, + index=index + ) + child = getattr(val, 'children', None) + if _is_nested_list(child): + _raise_invalid( + output=output, + bad_val=child, + outer_val=val, + bad_type=type(child).__name__, + path='', + index=index + ) + if isinstance(output_value, list): + for i, val in enumerate(output_value): + if isinstance(val, list): + _raise_invalid( + output=output, + bad_val=val, + outer_val=output_value, + bad_type=type(val).__name__, + path='', + ) + _validate_value(val, index=i) + else: + _validate_value(output_value) diff --git a/dash/dash.py b/dash/dash.py index 7e64b7a973..c81aef2e7d 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -24,6 +24,8 @@ from ._utils import AttributeDict as _AttributeDict from ._utils import interpolate_str as _interpolate from ._utils import format_tag as _format_tag +from ._utils import (_validate_callback_output, + _validate_children_callback_output) _default_index = ''' @@ -639,110 +641,6 @@ def _validate_callback(self, output, inputs, state, events): output.component_id, output.component_property).replace(' ', '')) - def _validate_callback_output(self, output_value, output): - valid = [str, dict, int, float, type(None), Component] - - def _raise_invalid(bad_val, outer_val, bad_type, path, index=None, - toplevel=False): - 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 property `{property:s}` of component `{id: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( - property=output.component_property, - id=output.component_id, - 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): - 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, - bad_type=type(j).__name__, - path=p, - index=index - ) - - # Children that are not of type Component or - # collections.MutableSequence not returned by traverse - child = getattr(j, 'children', None) - if not isinstance(child, collections.MutableSequence): - if child and not _value_is_valid(child): - _raise_invalid( - bad_val=child, - outer_val=val, - bad_type=type(child).__name__, - 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, collections.MutableSequence): - if child and not _value_is_valid(child): - _raise_invalid( - bad_val=child, - outer_val=val, - bad_type=type(child).__name__, - 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__, - bad_type=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) - # TODO - Update nomenclature. # "Parents" and "Children" should refer to the DOM tree # and not the dependency tree. @@ -781,6 +679,9 @@ def wrap_func(func): def add_context(*args, **kwargs): output_value = func(*args, **kwargs) + if output.component_property == 'children': + _validate_children_callback_output(output_value, + output) response = { 'response': { 'props': { @@ -795,7 +696,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