Skip to content

Commit

Permalink
Merge pull request #3051 from plotly/fix/bg-cookies
Browse files Browse the repository at this point in the history
Add request data to callback_context
  • Loading branch information
T4rk1n authored Oct 23, 2024
2 parents e6a9846 + dd5e4d6 commit 0d9cd2c
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 6 additions & 13 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
46 changes: 46 additions & 0 deletions dash/_callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions tests/integration/long_callback/app_ctx_cookies.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions tests/integration/long_callback/test_ctx_cookies.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit 0d9cd2c

Please sign in to comment.