diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b860dc41..07aae6781d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3034](https://github.com/plotly/dash/pull/3034) Remove whitespace from `metadata.json` files to reduce package size. - [#3009](https://github.com/plotly/dash/pull/3009) Performance improvement on (pattern-matching) callbacks. - [3028](https://github.com/plotly/dash/pull/3028) Fix jupyterlab v4 support. +- [#3051](https://github.com/plotly/dash/pull/3051) Add missing request data to callback context. Fix [#2235](https://github.com/plotly/dash/issues/2235). ## [2.18.1] - 2024-09-12 diff --git a/dash/_callback.py b/dash/_callback.py index 2eeda1c028..071c209dec 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -409,23 +409,16 @@ def add_context(*args, **kwargs): job_fn = callback_manager.func_registry.get(long_key) + ctx_value = AttributeDict(**context_value.get()) + ctx_value.ignore_register_page = True + ctx_value.pop("background_callback_manager") + ctx_value.pop("dash_response") + job = callback_manager.call_job_fn( cache_key, job_fn, func_args if func_args else func_kwargs, - AttributeDict( - args_grouping=callback_ctx.args_grouping, - using_args_grouping=callback_ctx.using_args_grouping, - outputs_grouping=callback_ctx.outputs_grouping, - using_outputs_grouping=callback_ctx.using_outputs_grouping, - inputs_list=callback_ctx.inputs_list, - states_list=callback_ctx.states_list, - outputs_list=callback_ctx.outputs_list, - input_values=callback_ctx.input_values, - state_values=callback_ctx.state_values, - triggered_inputs=callback_ctx.triggered_inputs, - ignore_register_page=True, - ), + ctx_value, ) data = { diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 756b2c36b3..42e4c506d9 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -30,6 +30,10 @@ def _get_context_value(): return context_value.get() +def _get_from_context(key, default): + return getattr(_get_context_value(), key, default) + + class FalsyList(list): def __bool__(self): # for Python 3 @@ -258,6 +262,48 @@ def set_props(self, component_id: typing.Union[str, dict], props: dict): else: ctx_value.updated_props[_id] = props + @property + @has_context + def cookies(self): + """ + Get the cookies for the current callback. + Works with background callbacks. + """ + return _get_from_context("cookies", {}) + + @property + @has_context + def headers(self): + """ + Get the original headers for the current callback. + Works with background callbacks. + """ + return _get_from_context("headers", {}) + + @property + @has_context + def path(self): + """ + Path of the callback request with the query parameters. + """ + return _get_from_context("path", "") + + @property + @has_context + def remote(self): + """ + Remote addr of the callback request. + """ + return _get_from_context("remote", "") + + @property + @has_context + def origin(self): + """ + Origin of the callback request. + """ + return _get_from_context("origin", "") + callback_context = CallbackContext() diff --git a/dash/dash.py b/dash/dash.py index 29fc7e4d63..3ad375c823 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1360,6 +1360,12 @@ def dispatch(self): g.using_outputs_grouping = [] g.updated_props = {} + g.cookies = dict(**flask.request.cookies) + g.headers = dict(**flask.request.headers) + g.path = flask.request.full_path + g.remote = flask.request.remote_addr + g.origin = flask.request.origin + except KeyError as missing_callback_function: msg = f"Callback function not found for output '{output}', perhaps you forgot to prepend the '@'?" raise KeyError(msg) from missing_callback_function diff --git a/tests/integration/long_callback/app_ctx_cookies.py b/tests/integration/long_callback/app_ctx_cookies.py new file mode 100644 index 0000000000..088e00a24c --- /dev/null +++ b/tests/integration/long_callback/app_ctx_cookies.py @@ -0,0 +1,43 @@ +from dash import Dash, Input, Output, html, callback, ctx + +from tests.integration.long_callback.utils import get_long_callback_manager + +long_callback_manager = get_long_callback_manager() +handle = long_callback_manager.handle + +app = Dash(__name__, background_callback_manager=long_callback_manager) + +app.layout = html.Div( + [ + html.Button("set-cookies", id="set-cookies"), + html.Button("use-cookies", id="use-cookies"), + html.Div(id="intermediate"), + html.Div("output", id="output"), + ] +) +app.test_lock = lock = long_callback_manager.test_lock + + +@callback( + Output("intermediate", "children"), + Input("set-cookies", "n_clicks"), + prevent_initial_call=True, +) +def set_cookies(_): + ctx.response.set_cookie("bg-cookie", "cookie-value") + return "ok" + + +@callback( + Output("output", "children"), + Input("use-cookies", "n_clicks"), + prevent_initial_call=True, + background=True, +) +def use_cookies(_): + value = ctx.cookies.get("bg-cookie") + return value + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/tests/integration/long_callback/test_ctx_cookies.py b/tests/integration/long_callback/test_ctx_cookies.py new file mode 100644 index 0000000000..e752d5fe20 --- /dev/null +++ b/tests/integration/long_callback/test_ctx_cookies.py @@ -0,0 +1,12 @@ +from tests.integration.long_callback.utils import setup_long_callback_app + + +def test_lcbc019_ctx_cookies(dash_duo, manager): + with setup_long_callback_app(manager, "app_ctx_cookies") as app: + dash_duo.start_server(app) + + dash_duo.find_element("#set-cookies").click() + dash_duo.wait_for_contains_text("#intermediate", "ok") + + dash_duo.find_element("#use-cookies").click() + dash_duo.wait_for_contains_text("#output", "cookie-value")