From 98c78ee9ac49363c91a4ee273a348b43d28d2938 Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Mon, 11 Apr 2022 19:18:32 +1000 Subject: [PATCH 1/7] Initial exploration of Promise handling in ClientSide Callbacks --- dash/dash-renderer/src/actions/callbacks.ts | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index bec4167493..a39af535e5 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -200,7 +200,7 @@ const getVals = (input: any) => const zipIfArray = (a: any, b: any) => Array.isArray(a) ? zip(a, b) : [[a, b]]; -function handleClientside( +async function handleClientside( dispatch: any, clientside_function: any, config: any, @@ -246,14 +246,10 @@ function handleClientside( dc.callback_context.states_list = state; dc.callback_context.states = stateDict; - const returnValue = dc[namespace][function_name](...args); + let returnValue = dc[namespace][function_name](...args); if (typeof returnValue?.then === 'function') { - throw new Error( - 'The clientside function returned a Promise. ' + - 'Promises are not supported in Dash clientside ' + - 'right now, but may be in the future.' - ); + returnValue = await returnValue; } zipIfArray(outputs, returnValue).forEach(([outi, reti]) => { @@ -504,15 +500,13 @@ export function executeCallback( if (clientside_function) { try { - return { - data: handleClientside( - dispatch, - clientside_function, - config, - payload - ), + const data = await handleClientside( + dispatch, + clientside_function, + config, payload - }; + ); + return {data, payload}; } catch (error: any) { return {error, payload}; } From 29ccdd89869b5df62ec50b7e92db82df478f8ecf Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Mon, 11 Apr 2022 19:48:35 +1000 Subject: [PATCH 2/7] Modify existing tests --- tests/integration/clientside/assets/clientside.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/clientside/assets/clientside.js b/tests/integration/clientside/assets/clientside.js index 71e8dc0abe..25823e0a99 100644 --- a/tests/integration/clientside/assets/clientside.js +++ b/tests/integration/clientside/assets/clientside.js @@ -53,7 +53,7 @@ window.dash_clientside.clientside = { 'side effect' ); }, 100); - resolve('foo'); + resolve('output'); }, 1); }); }, From 08180db7738ed80b83b933b5f0c39c155856adfa Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Mon, 11 Apr 2022 19:57:17 +1000 Subject: [PATCH 3/7] Added entry to CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b67f54c9b..4c7f9c1e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [2.3.2] + +### Added + +- [#2009](https://github.com/plotly/dash/pull/2009) Add support for Promises within Client-side callbacks as requested in [#1364](https://github.com/plotly/dash/pull/1364). + ## [2.3.1] - 2022-03-29 ### Fixed From 4d6f9cadc65180d28c9ffad2acf9b5dbe10fae53 Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Thu, 21 Apr 2022 18:39:06 +1000 Subject: [PATCH 4/7] Added 3 new tests for promise handling --- CHANGELOG.md | 2 +- .../clientside/assets/clientside.js | 26 +++++- .../integration/clientside/test_clientside.py | 91 ++++++++++++++++++- 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7f9c1e53..0e722c474b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). -## [2.3.2] +## [Unreleased] ### Added diff --git a/tests/integration/clientside/assets/clientside.js b/tests/integration/clientside/assets/clientside.js index 25823e0a99..6f851d1f6b 100644 --- a/tests/integration/clientside/assets/clientside.js +++ b/tests/integration/clientside/assets/clientside.js @@ -53,7 +53,7 @@ window.dash_clientside.clientside = { 'side effect' ); }, 100); - resolve('output'); + resolve('foo'); }, 1); }); }, @@ -98,5 +98,27 @@ window.dash_clientside.clientside = { } window.callCount += 1; return inputValue.toString(); - } + }, + + chained_promise: function (inputValue) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(inputValue + "-chained"); + }, 100); + }); + }, + + delayed_promise: function (inputValue) { + return new Promise(function (resolve) { + window.callbackDone = function (deferredValue) { + resolve("clientside-" + inputValue + "-" + deferredValue); + }; + }); + }, + + non_delayed_promise: function (inputValue) { + return new Promise(function (resolve) { + resolve("clientside-" + inputValue); + }); + }, }; diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py index 0160d93d06..38fe5d976a 100644 --- a/tests/integration/clientside/test_clientside.py +++ b/tests/integration/clientside/test_clientside.py @@ -1,5 +1,6 @@ # -*- coding: UTF-8 -*- -from multiprocessing import Value +from multiprocessing import Value, Lock +import pytest from dash import Dash, Input, Output, State, ClientsideFunction, ALL, html, dcc from selenium.webdriver.common.keys import Keys @@ -223,6 +224,7 @@ def test_clsd004_clientside_multiple_outputs(dash_duo): dash_duo.wait_for_text_to_equal(selector, expected) +@pytest.mark.xfail(reason="Promises are now handled within Dash-Renderer") def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo): app = Dash(__name__, assets_folder="assets") @@ -716,3 +718,90 @@ def test_clsd014_input_output_callback(dash_duo): assert call_count == 2, "initial + changed once" assert dash_duo.get_logs() == [] + + +def test_clsd015_clientside_chained_callbacks_returning_promise(dash_duo): + app = Dash(__name__, assets_folder="assets") + + app.layout = html.Div( + [ + html.Div(id="input", children=["initial"]), + html.Div(id="div-1"), + html.Div(id="div-2"), + ] + ) + + app.clientside_callback( + ClientsideFunction(namespace="clientside", function_name="chained_promise"), + Output("div-1", "children"), + Input("input", "children"), + ) + + @app.callback(Output("div-2", "children"), Input("div-1", "children")) + def callback(value): + return value + "-twice" + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#div-1", "initial-chained") + dash_duo.wait_for_text_to_equal("#div-2", "initial-chained-twice") + + +def test_clsd016_serverside_clientside_shared_input_with_promise(dash_duo): + app = Dash(__name__, assets_folder="assets") + + app.layout = html.Div( + [ + html.Div(id="input", children=["initial"]), + html.Div(id="clientside-div"), + html.Div(id="serverside-div"), + ] + ) + + app.clientside_callback( + ClientsideFunction(namespace="clientside", function_name="delayed_promise"), + Output("clientside-div", "children"), + Input("input", "children"), + ) + + @app.callback(Output("serverside-div", "children"), Input("input", "children")) + def callback(value): + return "serverside-" + value[0] + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#serverside-div", "serverside-initial") + dash_duo.driver.execute_script("window.callbackDone('deferred')") + dash_duo.wait_for_text_to_equal("#clientside-div", "clientside-initial-deferred") + + +def test_clsd017_clientside_serverside_shared_input_with_promise(dash_duo): + lock = Lock() + lock.acquire() + + app = Dash(__name__, assets_folder="assets") + + app.layout = html.Div( + [ + html.Div(id="input", children=["initial"]), + html.Div(id="clientside-div"), + html.Div(id="serverside-div"), + ] + ) + + app.clientside_callback( + ClientsideFunction(namespace="clientside", function_name="non_delayed_promise"), + Output("clientside-div", "children"), + Input("input", "children"), + ) + + @app.callback(Output("serverside-div", "children"), Input("input", "children")) + def callback(value): + with lock: + return "serverside-" + value[0] + "-deferred" + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#clientside-div", "clientside-initial") + lock.release() + dash_duo.wait_for_text_to_equal("#serverside-div", "serverside-initial-deferred") From 6decb16f165733cce6dada28ee965142215b89a4 Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Fri, 22 Apr 2022 02:45:58 +1000 Subject: [PATCH 5/7] inline async tests + dc.context_callback deletion --- dash/dash-renderer/src/actions/callbacks.ts | 4 +- .../integration/clientside/test_clientside.py | 76 ++++++++++++------- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index a39af535e5..a17fdf6dd9 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -248,6 +248,8 @@ async function handleClientside( let returnValue = dc[namespace][function_name](...args); + delete dc.callback_context; + if (typeof returnValue?.then === 'function') { returnValue = await returnValue; } @@ -270,8 +272,6 @@ async function handleClientside( throw e; } } finally { - delete dc.callback_context; - // Setting server = client forces network = 0 const totalTime = Date.now() - requestTime; const resources = { diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py index 38fe5d976a..feb72175dc 100644 --- a/tests/integration/clientside/test_clientside.py +++ b/tests/integration/clientside/test_clientside.py @@ -1,6 +1,5 @@ # -*- coding: UTF-8 -*- from multiprocessing import Value, Lock -import pytest from dash import Dash, Input, Output, State, ClientsideFunction, ALL, html, dcc from selenium.webdriver.common.keys import Keys @@ -224,31 +223,6 @@ def test_clsd004_clientside_multiple_outputs(dash_duo): dash_duo.wait_for_text_to_equal(selector, expected) -@pytest.mark.xfail(reason="Promises are now handled within Dash-Renderer") -def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo): - app = Dash(__name__, assets_folder="assets") - - app.layout = html.Div( - [ - html.Div(id="input", children="hello"), - html.Div(id="side-effect"), - html.Div(id="output", children="output"), - ] - ) - - app.clientside_callback( - ClientsideFunction("clientside", "side_effect_and_return_a_promise"), - Output("output", "children"), - [Input("input", "children")], - ) - - dash_duo.start_server(app) - - dash_duo.wait_for_text_to_equal("#input", "hello") - dash_duo.wait_for_text_to_equal("#side-effect", "side effect") - dash_duo.wait_for_text_to_equal("#output", "output") - - def test_clsd006_PreventUpdate(dash_duo): app = Dash(__name__, assets_folder="assets") @@ -805,3 +779,53 @@ def callback(value): dash_duo.wait_for_text_to_equal("#clientside-div", "clientside-initial") lock.release() dash_duo.wait_for_text_to_equal("#serverside-div", "serverside-initial-deferred") + + +def test_clsd018_clientside_inline_async_function(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Div(id="input", children=["initial"]), + html.Div(id="output-div"), + ] + ) + + app.clientside_callback( + """ + async function(input) { + return input + "-inline"; + } + """, + Output("output-div", "children"), + Input("input", "children"), + ) + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#output-div", "initial-inline") + + +def test_clsd019_clientside_inline_promise(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Div(id="input", children=["initial"]), + html.Div(id="output-div"), + ] + ) + + app.clientside_callback( + """ + function(inputValue) { + return new Promise(function (resolve) { + resolve(inputValue + "-inline"); + }); + } + """, + Output("output-div", "children"), + Input("input", "children"), + ) + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#output-div", "initial-nline") From 54e2e3d8135c96e5aed867bea9c3e058957ff280 Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Fri, 22 Apr 2022 03:17:39 +1000 Subject: [PATCH 6/7] Typo --- tests/integration/clientside/test_clientside.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py index feb72175dc..150da399e4 100644 --- a/tests/integration/clientside/test_clientside.py +++ b/tests/integration/clientside/test_clientside.py @@ -828,4 +828,4 @@ def test_clsd019_clientside_inline_promise(dash_duo): ) dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#output-div", "initial-nline") + dash_duo.wait_for_text_to_equal("#output-div", "initial-inline") From 6cd85bb3067593065175df4792445e8508a887f9 Mon Sep 17 00:00:00 2001 From: Lachlan Teale Date: Fri, 22 Apr 2022 08:46:14 +1000 Subject: [PATCH 7/7] additional deletion of context --- dash/dash-renderer/src/actions/callbacks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index a17fdf6dd9..4fafaab16b 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -272,6 +272,8 @@ async function handleClientside( throw e; } } finally { + delete dc.callback_context; + // Setting server = client forces network = 0 const totalTime = Date.now() - requestTime; const resources = {