From afea3bec0def455acf247c260fa663b774b5568f Mon Sep 17 00:00:00 2001 From: marekpiotrowski Date: Thu, 18 Nov 2021 10:28:09 +0100 Subject: [PATCH 1/4] Make callbacks directly callable (without the context wrapper) --- dash/_callback.py | 2 +- tests/unit/dash/test_grouped_callbacks.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 434613098f..57f018b6af 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -196,7 +196,7 @@ def add_context(*args, **kwargs): callback_map[callback_id]["callback"] = add_context - return add_context + return func return wrap_func diff --git a/tests/unit/dash/test_grouped_callbacks.py b/tests/unit/dash/test_grouped_callbacks.py index c7ecadde56..cdbe9dc124 100644 --- a/tests/unit/dash/test_grouped_callbacks.py +++ b/tests/unit/dash/test_grouped_callbacks.py @@ -1,5 +1,6 @@ import dash from dash._grouping import make_grouping_by_index, grouping_len, flatten_grouping +from dash._utils import create_callback_id from dash.dependencies import Input, State, Output, ClientsideFunction import mock import json @@ -39,12 +40,18 @@ def check_output_for_grouping(grouping): app = dash.Dash() mock_fn = mock.Mock() mock_fn.return_value = grouping + if multi: + callback_id = create_callback_id(flatten_grouping(outputs)) + else: + callback_id = create_callback_id(outputs) - wrapped_fn = app.callback( + app.callback( outputs, Input("input-a", "prop"), )(mock_fn) + wrapped_fn = app.callback_map[callback_id]["callback"] + expected_outputs = [ (dep.component_id, dep.component_property, val) for dep, val in zip(flatten_grouping(outputs), flatten_grouping(grouping)) @@ -96,11 +103,13 @@ def check_callback_inputs_for_grouping(grouping): mock_fn = mock.Mock() mock_fn.return_value = 23 - wrapped_fn = app.callback( + app.callback( Output("output-a", "prop"), inputs, )(mock_fn) + wrapped_fn = app.callback_map["output-a.prop"]["callback"] + flat_input_state_values = flatten_grouping(grouping) flat_input_values = flat_input_state_values[0::2] flat_state_values = flat_input_state_values[1::2] From 9ef0a648665a2c818afa2e305b8f759529cbd816 Mon Sep 17 00:00:00 2001 From: marekpiotrowski Date: Thu, 18 Nov 2021 10:48:49 +0100 Subject: [PATCH 2/4] Added integration test for calling callback directly --- .../callbacks/test_basic_callback.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c7e9df7db0..e1e6be15e0 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -382,7 +382,7 @@ def test_cbsc008_wildcard_prop_callbacks(dash_duo): "string", html.Div( id="output-1", - **{"data-cb": "initial value", "aria-cb": "initial value"} + **{"data-cb": "initial value", "aria-cb": "initial value"}, ), ] ) @@ -749,3 +749,24 @@ def update_output(value, data): assert store_data.value == 123 assert dash_duo.get_logs() == [] + + +def test_cbsc017_callback_directly_callable(dash_duo): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id="input", value="initial value"), + html.Div(html.Div([1.5, None, "string", html.Div(id="output-1")])), + ] + ) + + @app.callback( + Output("output-1", "children"), + [Input("input", "value")], + ) + def update_output(value): + return f"returning {value}" + + dash_duo.start_server(app) + + assert update_output("my-value") == "returning my-value" From d2a85e60775eb5f9b0e802ff5ee1b0aca8fc724f Mon Sep 17 00:00:00 2001 From: marekpiotrowski Date: Thu, 18 Nov 2021 13:59:20 +0100 Subject: [PATCH 3/4] Adjusted test_validation so that it uses wrapped function --- tests/integration/callbacks/test_validation.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/integration/callbacks/test_validation.py b/tests/integration/callbacks/test_validation.py index 4e3850e920..829cc4b009 100644 --- a/tests/integration/callbacks/test_validation.py +++ b/tests/integration/callbacks/test_validation.py @@ -77,10 +77,12 @@ def test_cbva002_callback_return_validation(): def single(a): return set([1]) + single_wrapped = app.callback_map["b.children"]["callback"] + with pytest.raises(InvalidCallbackReturnValue): # outputs_list (normally callback_context.outputs_list) is provided # by the dispatcher from the request. - single("aaa", outputs_list={"id": "b", "property": "children"}) + single_wrapped("aaa", outputs_list={"id": "b", "property": "children"}) pytest.fail("not serializable") @app.callback( @@ -89,12 +91,14 @@ def single(a): def multi(a): return [1, set([2])] + multi_wrapped = app.callback_map["..c.children...d.children.."]["callback"] + with pytest.raises(InvalidCallbackReturnValue): outputs_list = [ {"id": "c", "property": "children"}, {"id": "d", "property": "children"}, ] - multi("aaa", outputs_list=outputs_list) + multi_wrapped("aaa", outputs_list=outputs_list) pytest.fail("nested non-serializable") @app.callback( @@ -103,12 +107,14 @@ def multi(a): def multi2(a): return ["abc"] + multi2_wrapped = app.callback_map["..e.children...f.children.."]["callback"] + with pytest.raises(InvalidCallbackReturnValue): outputs_list = [ {"id": "e", "property": "children"}, {"id": "f", "property": "children"}, ] - multi2("aaa", outputs_list=outputs_list) + multi2_wrapped("aaa", outputs_list=outputs_list) pytest.fail("wrong-length list") From db25c42272865167385538f054194632c12ab111 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 6 May 2022 10:51:52 -0400 Subject: [PATCH 4/4] changelog for 1839 callback decorator change, and simplify its test --- CHANGELOG.md | 6 ++++-- tests/integration/callbacks/test_basic_callback.py | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa49dea96..50415e28d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,10 +38,12 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Changed -- [#2016](https://github.com/plotly/dash/pull/2016) Drop the 375px width from default percy_snapshot calls, keep only 1280px - - [#1751](https://github.com/plotly/dash/pull/1751) Rename `app.run_server` to `app.run` while preserving `app.run_server` for backwards compatibility. +- [#1839](https://github.com/plotly/dash/pull/1839) The `callback` decorator returns the original function, not the wrapped function, so that you can still call these functions directly, for example in tests. Note that in this case there will be no callback context so not all callbacks can be tested this way. + +- [#2016](https://github.com/plotly/dash/pull/2016) Drop the 375px width from default percy_snapshot calls, keep only 1280px + - [#2027](https://github.com/plotly/dash/pull/1751) Improve the error message when a user doesn't wrap children in a list ### Updated diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c527261bbf..0a3a4d59ee 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -767,7 +767,7 @@ def update_output(value, data): assert dash_duo.get_logs() == [] -def test_cbsc017_callback_directly_callable(dash_duo): +def test_cbsc017_callback_directly_callable(): app = Dash(__name__) app.layout = html.Div( [ @@ -783,6 +783,4 @@ def test_cbsc017_callback_directly_callable(dash_duo): def update_output(value): return f"returning {value}" - dash_duo.start_server(app) - assert update_output("my-value") == "returning my-value"