diff --git a/dash/dash.py b/dash/dash.py index 143d1e802b..3acf9f35cc 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -475,6 +475,110 @@ 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. @@ -521,9 +625,26 @@ def add_context(*args, **kwargs): } } + try: + jsonResponse = json.dumps( + response, + cls=plotly.utils.PlotlyJSONEncoder + ) + except TypeError: + self._validate_callback_output(output_value, output) + raise exceptions.InvalidCallbackReturnValue(''' + The callback for property `{property:s}` + of component `{id:s}` returned a value + which is not JSON serializable. + + 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)) + return flask.Response( - json.dumps(response, - cls=plotly.utils.PlotlyJSONEncoder), + jsonResponse, mimetype='application/json' ) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index d647df2411..2b3ad778aa 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -151,22 +151,36 @@ def __delitem__(self, id): # pylint: disable=redefined-builtin def traverse(self): """Yield each item in the tree.""" + for t in self.traverse_with_paths(): + yield t[1] + + def traverse_with_paths(self): + """Yield each item with its path in the tree.""" children = getattr(self, 'children', None) + children_type = type(children).__name__ + children_id = "(id={:s})".format(children.id) \ + if getattr(children, 'id', False) else '' + children_string = children_type + ' ' + children_id # children is just a component if isinstance(children, Component): - yield children - for t in children.traverse(): - yield t + yield "[*] " + children_string, children + for p, t in children.traverse_with_paths(): + yield "\n".join(["[*] " + children_string, p]), t # children is a list of components elif isinstance(children, collections.MutableSequence): - for i in children: # pylint: disable=not-an-iterable - yield i + for idx, i in enumerate(children): + list_path = "[{:d}] {:s} {}".format( + idx, + type(i).__name__, + "(id={:s})".format(i.id) if getattr(i, 'id', False) else '' + ) + yield list_path, i if isinstance(i, Component): - for t in i.traverse(): - yield t + for p, t in i.traverse_with_paths(): + yield "\n".join([list_path, p]), t def __iter__(self): """Yield IDs in the tree of children.""" diff --git a/dash/exceptions.py b/dash/exceptions.py index a8827d1faa..809b0b6843 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -44,3 +44,7 @@ class CantHaveMultipleOutputs(CallbackException): class PreventUpdate(CallbackException): pass + + +class InvalidCallbackReturnValue(CallbackException): + pass diff --git a/dev-requirements.txt b/dev-requirements.txt index 282b362edc..f667e354c3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -12,4 +12,4 @@ six plotly>=2.0.8 requests[security] flake8 -pylint +pylint==1.9.2