Skip to content

Commit b005c7e

Browse files
authored
Merge pull request #12 from bokeh/async_handlers
Add support for async handler functions
2 parents 29db47c + 063a20b commit b005c7e

File tree

1 file changed

+70
-8
lines changed

1 file changed

+70
-8
lines changed

bokeh_django/routing.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# -----------------------------------------------------------------------------
1111
from __future__ import annotations
1212

13+
import inspect
1314
import logging # isort:skip
1415
log = logging.getLogger(__name__)
1516

@@ -19,7 +20,7 @@
1920

2021
# Standard library imports
2122
from pathlib import Path
22-
from typing import Callable, List, Union
23+
from typing import Callable, List, Union, TYPE_CHECKING
2324
import weakref
2425

2526
# External imports
@@ -31,16 +32,29 @@
3132

3233
# Bokeh imports
3334
from bokeh.application import Application
35+
from bokeh.settings import settings as bokeh_settings
3436
from bokeh.application.handlers.document_lifecycle import DocumentLifecycleHandler
35-
from bokeh.application.handlers.function import FunctionHandler
37+
from bokeh.application.handlers.function import FunctionHandler, handle_exception
3638
from bokeh.command.util import build_single_handler_application, build_single_handler_applications
37-
from bokeh.server.contexts import ApplicationContext, BokehSessionContext, _RequestProxy, ServerSession
39+
from bokeh.server.contexts import (
40+
ApplicationContext,
41+
BokehSessionContext,
42+
_RequestProxy,
43+
ServerSession,
44+
ProtocolError,
45+
)
3846
from bokeh.document import Document
3947
from bokeh.util.token import get_token_payload
4048

4149
# Local imports
4250
from .consumers import AutoloadJsConsumer, DocConsumer, WSConsumer
4351

52+
if TYPE_CHECKING:
53+
from bokeh.server.contexts import (
54+
ID,
55+
HTTPServerRequest,
56+
)
57+
4458
# -----------------------------------------------------------------------------
4559
# Globals and constants
4660
# -----------------------------------------------------------------------------
@@ -49,7 +63,51 @@
4963
'RoutingConfiguration',
5064
)
5165

52-
ApplicationLike = Union[Application, Callable, Path]
66+
67+
class AsyncApplication(Application):
68+
async def create_document(self) -> Document:
69+
""" Creates and initializes a document using the Application's handlers.
70+
71+
"""
72+
doc = Document()
73+
await self.initialize_document(doc)
74+
return doc
75+
76+
async def initialize_document(self, doc: Document) -> None:
77+
""" Fills in a new document using the Application's handlers.
78+
79+
"""
80+
for h in self._handlers:
81+
result = h.modify_document(doc)
82+
if inspect.iscoroutine(result):
83+
await result
84+
if h.failed:
85+
log.error("Error running application handler %r: %s %s ", h, h.error, h.error_detail)
86+
87+
if bokeh_settings.perform_document_validation():
88+
doc.validate()
89+
90+
91+
class AsyncFunctionHandler(FunctionHandler):
92+
async def modify_document(self, doc: Document) -> None:
93+
""" Execute the configured ``func`` to modify the document.
94+
95+
After this method is first executed, ``safe_to_fork`` will return
96+
``False``.
97+
98+
"""
99+
try:
100+
await self._func(doc)
101+
except Exception as e:
102+
if self._trap_exceptions:
103+
handle_exception(self, e)
104+
else:
105+
raise
106+
finally:
107+
self._safe_to_fork = False
108+
109+
110+
ApplicationLike = Union[Application, Callable, Path, AsyncApplication]
53111

54112
# -----------------------------------------------------------------------------
55113
# General API
@@ -97,10 +155,12 @@ async def create_session_if_needed(self, session_id: ID, request: HTTPServerRequ
97155
except Exception as e:
98156
log.error("Failed to run session creation hooks %r", e, exc_info=True)
99157

100-
# This needs to be wrapped in the database_sync_to_async wrapper just in case the handler function accesses
101-
# Django ORM.
102-
103-
await database_sync_to_async(self._application.initialize_document)(doc)
158+
if isinstance(self._application, AsyncApplication):
159+
await self._application.initialize_document(doc)
160+
else:
161+
# This needs to be wrapped in the database_sync_to_async wrapper just in case the handler function
162+
# accesses Django ORM.
163+
await database_sync_to_async(self._application.initialize_document)(doc)
104164

105165
session = ServerSession(session_id, doc, io_loop=self._loop, token=token)
106166
del self._pending_sessions[session_id]
@@ -141,6 +201,8 @@ def __repr__(self):
141201

142202
def _normalize(self, obj: ApplicationLike) -> Application:
143203
if callable(obj):
204+
if inspect.iscoroutinefunction(obj):
205+
return AsyncApplication(AsyncFunctionHandler(obj, trap_exceptions=True))
144206
return Application(FunctionHandler(obj, trap_exceptions=True))
145207
elif isinstance(obj, Path):
146208
return build_single_handler_application(obj)

0 commit comments

Comments
 (0)