Skip to content

Commit e1002d5

Browse files
committed
adding support for async callbacks and page layouts
1 parent 071511c commit e1002d5

File tree

2 files changed

+62
-38
lines changed

2 files changed

+62
-38
lines changed

dash/_callback.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Callable, Optional, Any
55

66
import flask
7+
import asyncio
78

89
from .dependencies import (
910
handle_callback_args,
@@ -39,8 +40,13 @@
3940
from ._callback_context import context_value
4041

4142

42-
def _invoke_callback(func, *args, **kwargs): # used to mark the frame for the debugger
43-
return func(*args, **kwargs) # %% callback invoked %%
43+
async def _invoke_callback(func, *args, **kwargs):
44+
# Check if the function is a coroutine function
45+
if asyncio.iscoroutinefunction(func):
46+
return await func(*args, **kwargs)
47+
else:
48+
# If the function is not a coroutine, call it directly
49+
return func(*args, **kwargs)
4450

4551

4652
class NoUpdate:
@@ -353,7 +359,7 @@ def wrap_func(func):
353359
)
354360

355361
@wraps(func)
356-
def add_context(*args, **kwargs):
362+
async def add_context(*args, **kwargs):
357363
output_spec = kwargs.pop("outputs_list")
358364
app_callback_manager = kwargs.pop("long_callback_manager", None)
359365

@@ -493,7 +499,7 @@ def add_context(*args, **kwargs):
493499
return to_json(response)
494500
else:
495501
try:
496-
output_value = _invoke_callback(func, *func_args, **func_kwargs)
502+
output_value = await _invoke_callback(func, *func_args, **func_kwargs)
497503
except PreventUpdate as err:
498504
raise err
499505
except Exception as err: # pylint: disable=broad-exception-caught

dash/dash.py

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from typing import Any, Callable, Dict, Optional, Union, List
2020

2121
import flask
22+
import asyncio
2223

2324
from importlib_metadata import version as _get_distribution_version
2425

@@ -191,6 +192,14 @@ def _do_skip(error):
191192
# Singleton signal to not update an output, alternative to PreventUpdate
192193
no_update = _callback.NoUpdate() # pylint: disable=protected-access
193194

195+
async def execute_async_function(func, *args, **kwargs):
196+
# Check if the function is a coroutine function
197+
if asyncio.iscoroutinefunction(func):
198+
return await func(*args, **kwargs)
199+
else:
200+
# If the function is not a coroutine, call it directly
201+
return func(*args, **kwargs)
202+
194203

195204
# pylint: disable=too-many-instance-attributes
196205
# pylint: disable=too-many-arguments, too-many-locals
@@ -1267,7 +1276,7 @@ def long_callback(
12671276
)
12681277

12691278
# pylint: disable=R0915
1270-
def dispatch(self):
1279+
async def dispatch(self):
12711280
body = flask.request.get_json()
12721281

12731282
g = AttributeDict({})
@@ -1371,19 +1380,27 @@ def dispatch(self):
13711380
raise KeyError(msg) from missing_callback_function
13721381

13731382
ctx = copy_context()
1383+
# Create a partial function with the necessary arguments
13741384
# noinspection PyArgumentList
1385+
partial_func = functools.partial(
1386+
execute_async_function,
1387+
func,
1388+
*args,
1389+
outputs_list=outputs_list,
1390+
long_callback_manager=self._background_manager,
1391+
callback_context=g,
1392+
app=self,
1393+
app_on_error=self._on_error,
1394+
)
1395+
1396+
response_data = await ctx.run(partial_func)
1397+
1398+
# Check if the response is a coroutine
1399+
if asyncio.iscoroutine(response_data):
1400+
response_data = await response_data
1401+
13751402
response.set_data(
1376-
ctx.run(
1377-
functools.partial(
1378-
func,
1379-
*args,
1380-
outputs_list=outputs_list,
1381-
long_callback_manager=self._background_manager,
1382-
callback_context=g,
1383-
app=self,
1384-
app_on_error=self._on_error,
1385-
)
1386-
)
1403+
response_data
13871404
)
13881405
return response
13891406

@@ -2206,7 +2223,7 @@ def router():
22062223
inputs=inputs,
22072224
prevent_initial_call=True,
22082225
)
2209-
def update(pathname_, search_, **states):
2226+
async def update(pathname_, search_, **states):
22102227
"""
22112228
Updates dash.page_container layout on page navigation.
22122229
Updates the stored page title which will trigger the clientside callback to update the app title
@@ -2231,27 +2248,28 @@ def update(pathname_, search_, **states):
22312248
layout = page.get("layout", "")
22322249
title = page["title"]
22332250

2234-
if callable(layout):
2235-
layout = (
2236-
layout(**path_variables, **query_parameters, **states)
2237-
if path_variables
2238-
else layout(**query_parameters, **states)
2239-
)
2240-
if callable(title):
2241-
title = title(**path_variables) if path_variables else title()
2242-
2243-
return layout, {"title": title}
2244-
2245-
_validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY)
2246-
_validate.validate_registry(_pages.PAGE_REGISTRY)
2247-
2248-
# Set validation_layout
2249-
if not self.config.suppress_callback_exceptions:
2250-
self.validation_layout = html.Div(
2251-
[
2252-
page["layout"]() if callable(page["layout"]) else page["layout"]
2253-
for page in _pages.PAGE_REGISTRY.values()
2254-
]
2251+
if callable(layout):
2252+
layout = await execute_async_function(layout,
2253+
**{**(path_variables or {}), **query_parameters, **states}
2254+
)
2255+
if callable(title):
2256+
title = await execute_async_function(title,
2257+
**(path_variables or {})
2258+
)
2259+
2260+
return layout, {"title": title}
2261+
2262+
_validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY)
2263+
_validate.validate_registry(_pages.PAGE_REGISTRY)
2264+
2265+
# Set validation_layout
2266+
if not self.config.suppress_callback_exceptions:
2267+
self.validation_layout = html.Div(
2268+
[
2269+
asyncio.run(execute_async_function(page["layout"])) if callable(page["layout"]) else page[
2270+
"layout"]
2271+
for page in _pages.PAGE_REGISTRY.values()
2272+
]
22552273
+ [
22562274
# pylint: disable=not-callable
22572275
self.layout()

0 commit comments

Comments
 (0)