Skip to content

Commit 9336d29

Browse files
committed
test: add regression tests for dict-id JSON dot escaping in create_callback_id
1 parent 48ad23b commit 9336d29

1 file changed

Lines changed: 165 additions & 128 deletions

File tree

‎tests/unit/test_callback_unit.py‎

Lines changed: 165 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,165 @@
1-
"""Unit tests for callback decorator behavior - no browser required."""
2-
import inspect
3-
4-
import dash
5-
from dash import Input, Output, State, callback
6-
7-
8-
def test_callback_returns_callable():
9-
"""Test that callback returns a callable decorator."""
10-
decorator = callback(Output("output", "children"), Input("input", "value"))
11-
assert callable(decorator)
12-
13-
14-
def test_callback_decorates_function():
15-
"""Test that callback can decorate a function."""
16-
17-
@callback(Output("output", "children"), Input("input", "value"))
18-
def my_callback(value):
19-
return f"Value: {value}"
20-
21-
assert callable(my_callback)
22-
assert my_callback.__name__ == "my_callback"
23-
24-
25-
def test_callback_signature_includes_typed_options():
26-
"""Test that callback exposes the expected decorator keyword arguments."""
27-
sig = inspect.signature(callback)
28-
29-
expected = {
30-
"background",
31-
"interval",
32-
"progress",
33-
"progress_default",
34-
"running",
35-
"cancel",
36-
"manager",
37-
"cache_args_to_ignore",
38-
"cache_ignore_triggered",
39-
"on_error",
40-
"api_endpoint",
41-
"optional",
42-
"hidden",
43-
}
44-
assert expected.issubset(set(sig.parameters))
45-
46-
47-
def test_callback_with_multiple_inputs():
48-
"""Test callback with multiple inputs."""
49-
50-
@callback(
51-
Output("output", "children"),
52-
Input("input1", "value"),
53-
Input("input2", "value"),
54-
)
55-
def multi_input_callback(val1, val2):
56-
return f"{val1} + {val2}"
57-
58-
assert callable(multi_input_callback)
59-
60-
61-
def test_callback_with_state():
62-
"""Test callback with State."""
63-
64-
@callback(
65-
Output("output", "children"),
66-
Input("input", "value"),
67-
State("state", "value"),
68-
)
69-
def callback_with_state(input_val, state_val):
70-
return f"{input_val} - {state_val}"
71-
72-
assert callable(callback_with_state)
73-
74-
75-
def test_callback_with_multiple_outputs():
76-
"""Test callback with multiple outputs."""
77-
78-
@callback(
79-
Output("output1", "children"),
80-
Output("output2", "children"),
81-
Input("input", "value"),
82-
)
83-
def multi_output_callback(value):
84-
return value, f"Copy: {value}"
85-
86-
assert callable(multi_output_callback)
87-
88-
89-
def test_callback_preserves_docstring():
90-
"""Test that callback preserves the wrapped function's docstring."""
91-
92-
@callback(Output("output", "children"), Input("input", "value"))
93-
def documented_callback(value):
94-
"""This is a documented callback."""
95-
return value
96-
97-
assert documented_callback.__doc__ == "This is a documented callback."
98-
99-
100-
def test_callback_with_prevent_initial_call():
101-
"""Test callback with prevent_initial_call parameter."""
102-
103-
@callback(
104-
Output("output", "children"),
105-
Input("input", "value"),
106-
prevent_initial_call=True,
107-
)
108-
def callback_no_initial(value):
109-
return value
110-
111-
assert callable(callback_no_initial)
112-
113-
114-
def test_callback_with_background_params():
115-
"""Test that callback accepts background callback parameters."""
116-
decorator = callback(
117-
Output("output", "children"),
118-
Input("input", "value"),
119-
background=False,
120-
interval=1000,
121-
)
122-
assert callable(decorator)
123-
124-
125-
def test_callback_module_export():
126-
"""Test that callback is properly exported from dash module."""
127-
assert hasattr(dash, "callback")
128-
assert dash.callback is callback
1+
"""Unit tests for callback decorator behavior - no browser required."""
2+
import inspect
3+
import json
4+
5+
import dash
6+
from dash import Input, Output, State, callback
7+
from dash._utils import create_callback_id
8+
9+
10+
def test_callback_returns_callable():
11+
"""Test that callback returns a callable decorator."""
12+
decorator = callback(Output("output", "children"), Input("input", "value"))
13+
assert callable(decorator)
14+
15+
16+
def test_callback_decorates_function():
17+
"""Test that callback can decorate a function."""
18+
19+
@callback(Output("output", "children"), Input("input", "value"))
20+
def my_callback(value):
21+
return f"Value: {value}"
22+
23+
assert callable(my_callback)
24+
assert my_callback.__name__ == "my_callback"
25+
26+
27+
def test_callback_signature_includes_typed_options():
28+
"""Test that callback exposes the expected decorator keyword arguments."""
29+
sig = inspect.signature(callback)
30+
31+
expected = {
32+
"background",
33+
"interval",
34+
"progress",
35+
"progress_default",
36+
"running",
37+
"cancel",
38+
"manager",
39+
"cache_args_to_ignore",
40+
"cache_ignore_triggered",
41+
"on_error",
42+
"api_endpoint",
43+
"optional",
44+
"hidden",
45+
}
46+
assert expected.issubset(set(sig.parameters))
47+
48+
49+
def test_callback_with_multiple_inputs():
50+
"""Test callback with multiple inputs."""
51+
52+
@callback(
53+
Output("output", "children"),
54+
Input("input1", "value"),
55+
Input("input2", "value"),
56+
)
57+
def multi_input_callback(val1, val2):
58+
return f"{val1} + {val2}"
59+
60+
assert callable(multi_input_callback)
61+
62+
63+
def test_callback_with_state():
64+
"""Test callback with State."""
65+
66+
@callback(
67+
Output("output", "children"),
68+
Input("input", "value"),
69+
State("state", "value"),
70+
)
71+
def callback_with_state(input_val, state_val):
72+
return f"{input_val} - {state_val}"
73+
74+
assert callable(callback_with_state)
75+
76+
77+
def test_callback_with_multiple_outputs():
78+
"""Test callback with multiple outputs."""
79+
80+
@callback(
81+
Output("output1", "children"),
82+
Output("output2", "children"),
83+
Input("input", "value"),
84+
)
85+
def multi_output_callback(value):
86+
return value, f"Copy: {value}"
87+
88+
assert callable(multi_output_callback)
89+
90+
91+
def test_callback_preserves_docstring():
92+
"""Test that callback preserves the wrapped function's docstring."""
93+
94+
@callback(Output("output", "children"), Input("input", "value"))
95+
def documented_callback(value):
96+
"""This is a documented callback."""
97+
return value
98+
99+
assert documented_callback.__doc__ == "This is a documented callback."
100+
101+
102+
def test_callback_with_prevent_initial_call():
103+
"""Test callback with prevent_initial_call parameter."""
104+
105+
@callback(
106+
Output("output", "children"),
107+
Input("input", "value"),
108+
prevent_initial_call=True,
109+
)
110+
def callback_no_initial(value):
111+
return value
112+
113+
assert callable(callback_no_initial)
114+
115+
116+
def test_callback_with_background_params():
117+
"""Test that callback accepts background callback parameters."""
118+
decorator = callback(
119+
Output("output", "children"),
120+
Input("input", "value"),
121+
background=False,
122+
interval=1000,
123+
)
124+
assert callable(decorator)
125+
126+
127+
def test_callback_module_export():
128+
"""Test that callback is properly exported from dash module."""
129+
assert hasattr(dash, "callback")
130+
assert dash.callback is callback
131+
132+
133+
def test_create_callback_id_escapes_dots_in_string_id():
134+
"""A dot in a plain string component id is escaped with a backslash."""
135+
output = Output("my.component", "children")
136+
callback_id = create_callback_id(output, [])
137+
138+
assert callback_id == "my\\.component.children"
139+
140+
141+
def test_create_callback_id_escapes_dots_in_dict_id_as_json_unicode():
142+
"""A dot in a dict id must use the JSON \\u002e escape, not \\.,
143+
otherwise the frontend's JSON.parse throws a SyntaxError when it
144+
un-escapes the id portion of the callback id string (see #3480)."""
145+
output = Output({"type": "my.type", "index": 1}, "children")
146+
callback_id = create_callback_id(output, [])
147+
148+
id_part, prop_part = callback_id.rsplit(".", 1)
149+
assert prop_part == "children"
150+
# The escaped id must not contain a raw backslash-dot sequence...
151+
assert "\\." not in id_part
152+
# ...and must be valid JSON once the . escape is present verbatim.
153+
assert "\\u002e" in id_part
154+
parsed = json.loads(id_part)
155+
assert parsed == {"type": "my.type", "index": 1}
156+
157+
158+
def test_create_callback_id_dict_id_without_dots_unaffected():
159+
"""Dict ids with no dots in their values still round-trip through JSON."""
160+
output = Output({"type": "widget", "index": 2}, "value")
161+
callback_id = create_callback_id(output, [])
162+
163+
id_part, prop_part = callback_id.rsplit(".", 1)
164+
assert prop_part == "value"
165+
assert json.loads(id_part) == {"type": "widget", "index": 2}

0 commit comments

Comments
 (0)