From c8c756c70eb4d4937cd8457f93635d2a45a69d32 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 20 Feb 2023 11:32:38 -0500 Subject: [PATCH] Validate allow_duplicate with prevent_initial_call. --- dash/_callback.py | 8 ++++++- dash/_validate.py | 27 ++++++++++++++++++++++ tests/unit/library/test_validate.py | 35 +++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 612bc83c7c..7e099e0de1 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -241,13 +241,19 @@ def insert_callback( if prevent_initial_call is None: prevent_initial_call = config_prevent_initial_callbacks + _validate.validate_duplicate_output( + output, prevent_initial_call, config_prevent_initial_callbacks + ) + callback_id = create_callback_id(output) callback_spec = { "output": callback_id, "inputs": [c.to_dict() for c in inputs], "state": [c.to_dict() for c in state], "clientside_function": None, - "prevent_initial_call": prevent_initial_call, + # prevent_initial_call can be a string "initial_duplicates" + # which should not prevent the initial call. + "prevent_initial_call": prevent_initial_call is True, "long": long and { "interval": long["interval"], diff --git a/dash/_validate.py b/dash/_validate.py index 02b1018ac8..ba33b514b0 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -521,3 +521,30 @@ def validate_long_callbacks(callback_map): f"Long callback circular error!\n{circular} is used as input for a long callback" f" but also used as output from an input that is updated with progress or running argument." ) + + +def validate_duplicate_output( + output, prevent_initial_call, config_prevent_initial_call +): + + if "initial_duplicate" in (prevent_initial_call, config_prevent_initial_call): + return + + def _valid(out): + if out.allow_duplicate is True and not ( + prevent_initial_call or config_prevent_initial_call + ): + raise exceptions.DuplicateCallback( + "allow_duplicate requires prevent_initial_call to be True. The order of the call is not" + " guaranteed to be the same on every page load. " + "To enable duplicate callback with initial call, set prevent_initial_call='initial_duplicate' " + " or globally in the config prevent_initial_callbacks='initial_duplicate'" + ) + + if isinstance(output, (list, tuple)): + for o in output: + _valid(o) + + return + + _valid(output) diff --git a/tests/unit/library/test_validate.py b/tests/unit/library/test_validate.py index db0f3da7fc..8dc4e85d15 100644 --- a/tests/unit/library/test_validate.py +++ b/tests/unit/library/test_validate.py @@ -2,8 +2,8 @@ from dash import Output from dash.html import Div -from dash.exceptions import InvalidCallbackReturnValue -from dash._validate import fail_callback_output +from dash.exceptions import InvalidCallbackReturnValue, DuplicateCallback +from dash._validate import fail_callback_output, validate_duplicate_output @pytest.mark.parametrize( @@ -36,3 +36,34 @@ def test_ddvl001_fail_handler_fails_correctly(val): with pytest.raises(InvalidCallbackReturnValue): fail_callback_output(val, outputs) + + +@pytest.mark.parametrize( + "output, prevent_initial_call, config_prevent_initial_call, expect_error", + [ + (Output("a", "a", allow_duplicate=True), True, False, False), + (Output("a", "a", allow_duplicate=True), False, True, False), + (Output("a", "a", allow_duplicate=True), True, True, False), + (Output("a", "a", allow_duplicate=True), False, False, True), + (Output("a", "a", allow_duplicate=True), "initial_duplicates", False, False), + (Output("a", "a", allow_duplicate=True), False, "initial_duplicates", False), + (Output("a", "a"), False, False, False), + ], +) +def test_ddv002_allow_duplicate_validation( + output, prevent_initial_call, config_prevent_initial_call, expect_error +): + valid = False + if expect_error: + with pytest.raises(DuplicateCallback) as err: + validate_duplicate_output( + output, prevent_initial_call, config_prevent_initial_call + ) + valid = err is not None + else: + validate_duplicate_output( + output, prevent_initial_call, config_prevent_initial_call + ) + valid = True + + assert valid