|
10 | 10 | # -----------------------------------------------------------------------------
|
11 | 11 | from __future__ import annotations
|
12 | 12 |
|
| 13 | +import inspect |
13 | 14 | import logging # isort:skip
|
14 | 15 | log = logging.getLogger(__name__)
|
15 | 16 |
|
|
19 | 20 |
|
20 | 21 | # Standard library imports
|
21 | 22 | from pathlib import Path
|
22 |
| -from typing import Callable, List, Union |
| 23 | +from typing import Callable, List, Union, TYPE_CHECKING |
23 | 24 | import weakref
|
24 | 25 |
|
25 | 26 | # External imports
|
|
31 | 32 |
|
32 | 33 | # Bokeh imports
|
33 | 34 | from bokeh.application import Application
|
| 35 | +from bokeh.settings import settings as bokeh_settings |
34 | 36 | 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 |
36 | 38 | 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 | +) |
38 | 46 | from bokeh.document import Document
|
39 | 47 | from bokeh.util.token import get_token_payload
|
40 | 48 |
|
41 | 49 | # Local imports
|
42 | 50 | from .consumers import AutoloadJsConsumer, DocConsumer, WSConsumer
|
43 | 51 |
|
| 52 | +if TYPE_CHECKING: |
| 53 | + from bokeh.server.contexts import ( |
| 54 | + ID, |
| 55 | + HTTPServerRequest, |
| 56 | + ) |
| 57 | + |
44 | 58 | # -----------------------------------------------------------------------------
|
45 | 59 | # Globals and constants
|
46 | 60 | # -----------------------------------------------------------------------------
|
|
49 | 63 | 'RoutingConfiguration',
|
50 | 64 | )
|
51 | 65 |
|
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] |
53 | 111 |
|
54 | 112 | # -----------------------------------------------------------------------------
|
55 | 113 | # General API
|
@@ -97,10 +155,12 @@ async def create_session_if_needed(self, session_id: ID, request: HTTPServerRequ
|
97 | 155 | except Exception as e:
|
98 | 156 | log.error("Failed to run session creation hooks %r", e, exc_info=True)
|
99 | 157 |
|
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) |
104 | 164 |
|
105 | 165 | session = ServerSession(session_id, doc, io_loop=self._loop, token=token)
|
106 | 166 | del self._pending_sessions[session_id]
|
@@ -141,6 +201,8 @@ def __repr__(self):
|
141 | 201 |
|
142 | 202 | def _normalize(self, obj: ApplicationLike) -> Application:
|
143 | 203 | if callable(obj):
|
| 204 | + if inspect.iscoroutinefunction(obj): |
| 205 | + return AsyncApplication(AsyncFunctionHandler(obj, trap_exceptions=True)) |
144 | 206 | return Application(FunctionHandler(obj, trap_exceptions=True))
|
145 | 207 | elif isinstance(obj, Path):
|
146 | 208 | return build_single_handler_application(obj)
|
|
0 commit comments