Skip to content

Commit 00bedbb

Browse files
committed
test that fail_callback_output always throws the right kind of error
rather than a built-in error because it had its own problem!
1 parent d0d8adb commit 00bedbb

File tree

2 files changed

+84
-14
lines changed

2 files changed

+84
-14
lines changed

dash/_validate.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import collections
1+
from collections.abc import MutableSequence
22
import re
33
from textwrap import dedent
44

55
from ._grouping import grouping_len, map_grouping
66
from .development.base_component import Component
77
from . import exceptions
8-
from ._utils import patch_collections_abc, stringify_id
8+
from ._utils import patch_collections_abc, stringify_id, to_json
99

1010

1111
def validate_callback(outputs, inputs, state, extra_args, types):
@@ -198,7 +198,8 @@ def validate_multi_return(outputs_list, output_value, callback_id):
198198

199199

200200
def fail_callback_output(output_value, output):
201-
valid = (str, dict, int, float, type(None), Component)
201+
valid_children = (str, int, float, type(None), Component)
202+
valid_props = (str, int, float, type(None), tuple, MutableSequence)
202203

203204
def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False):
204205
bad_type = type(bad_val).__name__
@@ -247,43 +248,74 @@ def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False):
247248
)
248249
)
249250

250-
def _value_is_valid(val):
251-
return isinstance(val, valid)
251+
def _valid_child(val):
252+
return isinstance(val, valid_children)
253+
254+
def _valid_prop(val):
255+
return isinstance(val, valid_props)
256+
257+
def _can_serialize(val):
258+
if not (_valid_child(val) or _valid_prop(val)):
259+
return False
260+
try:
261+
to_json(val)
262+
except TypeError:
263+
return False
264+
return True
252265

253266
def _validate_value(val, index=None):
254267
# val is a Component
255268
if isinstance(val, Component):
269+
unserializable_items = []
256270
# pylint: disable=protected-access
257271
for p, j in val._traverse_with_paths():
258272
# check each component value in the tree
259-
if not _value_is_valid(j):
273+
if not _valid_child(j):
260274
_raise_invalid(bad_val=j, outer_val=val, path=p, index=index)
261275

276+
if not _can_serialize(j):
277+
# collect unserializable items separately, so we can report
278+
# only the deepest level, not all the parent components that
279+
# are just unserializable because of their children.
280+
unserializable_items = [
281+
i for i in unserializable_items if not p.startswith(i[0])
282+
]
283+
if unserializable_items:
284+
# we already have something unserializable in a different
285+
# branch - time to stop and fail
286+
break
287+
if all(not i[0].startswith(p) for i in unserializable_items):
288+
unserializable_items.append((p, j))
289+
262290
# Children that are not of type Component or
263291
# list/tuple not returned by traverse
264292
child = getattr(j, "children", None)
265-
if not isinstance(child, (tuple, collections.MutableSequence)):
266-
if child and not _value_is_valid(child):
293+
if not isinstance(child, (tuple, MutableSequence)):
294+
if child and not _can_serialize(child):
267295
_raise_invalid(
268296
bad_val=child,
269297
outer_val=val,
270298
path=p + "\n" + "[*] " + type(child).__name__,
271299
index=index,
272300
)
301+
if unserializable_items:
302+
p, j = unserializable_items[0]
303+
# just report the first one, even if there are multiple,
304+
# as that's how all the other errors work
305+
_raise_invalid(bad_val=j, outer_val=val, path=p, index=index)
273306

274307
# Also check the child of val, as it will not be returned
275308
child = getattr(val, "children", None)
276-
if not isinstance(child, (tuple, collections.MutableSequence)):
277-
if child and not _value_is_valid(child):
309+
if not isinstance(child, (tuple, MutableSequence)):
310+
if child and not _can_serialize(val):
278311
_raise_invalid(
279312
bad_val=child,
280313
outer_val=val,
281314
path=type(child).__name__,
282315
index=index,
283316
)
284317

285-
# val is not a Component, but is at the top level of tree
286-
elif not _value_is_valid(val):
318+
if not _can_serialize(val):
287319
_raise_invalid(
288320
bad_val=val,
289321
outer_val=type(val).__name__,
@@ -301,13 +333,13 @@ def _validate_value(val, index=None):
301333
# if we got this far, raise a generic JSON error
302334
raise exceptions.InvalidCallbackReturnValue(
303335
"""
304-
The callback for property `{property:s}` of component `{id:s}`
336+
The callback for output `{output}`
305337
returned a value which is not JSON serializable.
306338
307339
In general, Dash properties can only be dash components, strings,
308340
dictionaries, numbers, None, or lists of those.
309341
""".format(
310-
property=output.component_property, id=output.component_id
342+
output=repr(output)
311343
)
312344
)
313345

tests/unit/dash/test_validate.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
3+
from dash import Output
4+
from dash.html import Div
5+
from dash.exceptions import InvalidCallbackReturnValue
6+
from dash._validate import fail_callback_output
7+
8+
9+
@pytest.mark.parametrize(
10+
"val",
11+
[
12+
{0},
13+
[{1}, 1],
14+
[1, {2}],
15+
Div({3}),
16+
Div([{4}]),
17+
Div(style={5}),
18+
Div([1, {6}]),
19+
Div([1, Div({7})]),
20+
[Div({8}), 1],
21+
[1, Div({9})],
22+
[Div(Div({10}))],
23+
[Div(Div({11})), 1],
24+
[1, Div(Div({12}))],
25+
{"a": {13}},
26+
Div(style={"a": {14}}),
27+
Div(style=[{15}]),
28+
[1, Div(style=[{16}])],
29+
],
30+
)
31+
def test_ddvl001_fail_handler_fails_correctly(val):
32+
if isinstance(val, list):
33+
outputs = [Output(f"id{i}", "children") for i in range(len(val))]
34+
else:
35+
outputs = Output("id", "children")
36+
37+
with pytest.raises(InvalidCallbackReturnValue):
38+
fail_callback_output(val, outputs)

0 commit comments

Comments
 (0)