Skip to content

Commit 6b6aca2

Browse files
authored
Merge pull request #2 from bokeh/django3.2_compatibility
Django3.2 compatibility
2 parents 208095e + 258c4bc commit 6b6aca2

File tree

20 files changed

+782
-121
lines changed

20 files changed

+782
-121
lines changed

bokeh_django/__init__.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33

44
# Bokeh imports
55
from .apps import DjangoBokehConfig
6+
from .consumers import AutoloadJsConsumer, WSConsumer
67
from .routing import autoload, directory, document
78
from .static import static_extensions
89

9-
import_required("django", "django is required by bokeh.server.django")
10-
import_required("channels", "The package channels is required by bokeh.server.django and must be installed")
11-
12-
default_app_config = "bokeh.server.django.DjangoBokehConfig"
10+
import_required("django", "django is required by bokeh-django")
11+
import_required("channels", "The package channels is required by bokeh-django and must be installed")

bokeh_django/apps.py

+19-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
#-----------------------------------------------------------------------------
1+
# -----------------------------------------------------------------------------
22
# Copyright (c) 2012 - 2022, Anaconda, Inc., and Bokeh Contributors.
33
# All rights reserved.
44
#
55
# The full license is in the file LICENSE.txt, distributed with this software.
6-
#-----------------------------------------------------------------------------
6+
# -----------------------------------------------------------------------------
77

8-
#-----------------------------------------------------------------------------
8+
# -----------------------------------------------------------------------------
99
# Boilerplate
10-
#-----------------------------------------------------------------------------
10+
# -----------------------------------------------------------------------------
1111
from __future__ import annotations
1212

1313
import logging # isort:skip
1414
log = logging.getLogger(__name__)
1515

16-
#-----------------------------------------------------------------------------
16+
# -----------------------------------------------------------------------------
1717
# Imports
18-
#-----------------------------------------------------------------------------
18+
# -----------------------------------------------------------------------------
1919

2020
# Standard library imports
2121
from importlib import import_module
@@ -28,21 +28,23 @@
2828
# Bokeh imports
2929
from .routing import Routing, RoutingConfiguration
3030

31-
#-----------------------------------------------------------------------------
31+
# -----------------------------------------------------------------------------
3232
# Globals and constants
33-
#-----------------------------------------------------------------------------
33+
# -----------------------------------------------------------------------------
3434

3535
__all__ = (
3636
'DjangoBokehConfig',
3737
)
3838

39-
#-----------------------------------------------------------------------------
39+
# -----------------------------------------------------------------------------
4040
# General API
41-
#-----------------------------------------------------------------------------
41+
# -----------------------------------------------------------------------------
42+
4243

4344
class DjangoBokehConfig(AppConfig):
4445

45-
name = label = 'bokeh.server.django'
46+
name = 'bokeh_django'
47+
label = 'bokeh_django'
4648

4749
_routes: RoutingConfiguration | None = None
4850

@@ -58,14 +60,14 @@ def routes(self) -> RoutingConfiguration:
5860
self._routes = RoutingConfiguration(self.bokeh_apps)
5961
return self._routes
6062

61-
#-----------------------------------------------------------------------------
63+
# -----------------------------------------------------------------------------
6264
# Dev API
63-
#-----------------------------------------------------------------------------
65+
# -----------------------------------------------------------------------------
6466

65-
#-----------------------------------------------------------------------------
67+
# -----------------------------------------------------------------------------
6668
# Private API
67-
#-----------------------------------------------------------------------------
69+
# -----------------------------------------------------------------------------
6870

69-
#-----------------------------------------------------------------------------
71+
# -----------------------------------------------------------------------------
7072
# Code
71-
#-----------------------------------------------------------------------------
73+
# -----------------------------------------------------------------------------

bokeh_django/consumers.py

+83-53
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
#-----------------------------------------------------------------------------
1+
# -----------------------------------------------------------------------------
22
# Copyright (c) 2012 - 2022, Anaconda, Inc., and Bokeh Contributors.
33
# All rights reserved.
44
#
55
# The full license is in the file LICENSE.txt, distributed with this software.
6-
#-----------------------------------------------------------------------------
6+
# -----------------------------------------------------------------------------
77

8-
#-----------------------------------------------------------------------------
8+
# -----------------------------------------------------------------------------
99
# Boilerplate
10-
#-----------------------------------------------------------------------------
10+
# -----------------------------------------------------------------------------
1111
from __future__ import annotations
1212

1313
import logging # isort:skip
1414
log = logging.getLogger(__name__)
1515

16-
#-----------------------------------------------------------------------------
16+
# -----------------------------------------------------------------------------
1717
# Imports
18-
#-----------------------------------------------------------------------------
18+
# -----------------------------------------------------------------------------
1919

2020
# Standard library imports
2121
import asyncio
@@ -56,19 +56,20 @@
5656
get_token_payload,
5757
)
5858

59-
#-----------------------------------------------------------------------------
59+
# -----------------------------------------------------------------------------
6060
# Globals and constants
61-
#-----------------------------------------------------------------------------
61+
# -----------------------------------------------------------------------------
6262

6363
__all__ = (
6464
'DocConsumer',
6565
'AutoloadJsConsumer',
6666
'WSConsumer',
6767
)
6868

69-
#-----------------------------------------------------------------------------
69+
# -----------------------------------------------------------------------------
7070
# General API
71-
#-----------------------------------------------------------------------------
71+
# -----------------------------------------------------------------------------
72+
7273

7374
class ConsumerHelper(AsyncConsumer):
7475

@@ -95,34 +96,45 @@ def resources(self, absolute_url: str | None = None) -> Resources:
9596
return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)
9697
return Resources(mode=mode)
9798

99+
98100
class SessionConsumer(AsyncHttpConsumer, ConsumerHelper):
99101

100-
application_context: ApplicationContext
102+
_application_context: ApplicationContext
101103

102-
def __init__(self, scope: Dict[str, Any]) -> None:
103-
super().__init__(scope)
104+
def __init__(self, *args: Any, **kwargs: Any) -> None:
105+
super().__init__(*args, **kwargs)
106+
self._application_context = kwargs.get('app_context')
104107

105-
kwargs = self.scope["url_route"]["kwargs"]
106-
self.application_context = kwargs["app_context"]
108+
@property
109+
def application_context(self) -> ApplicationContext:
110+
# backwards compatibility
111+
if self._application_context is None:
112+
self._application_context = self.scope["url_route"]["kwargs"]["app_context"]
107113

108114
# XXX: accessing asyncio's IOLoop directly doesn't work
109-
if self.application_context.io_loop is None:
110-
self.application_context._loop = IOLoop.current()
115+
if self._application_context.io_loop is None:
116+
self._application_context._loop = IOLoop.current()
117+
return self._application_context
111118

112119
async def _get_session(self) -> ServerSession:
113120
session_id = self.arguments.get('bokeh-session-id',
114121
generate_session_id(secret_key=None, signed=False))
115-
payload = {'headers': {k.decode('utf-8'): v.decode('utf-8')
116-
for k, v in self.request.headers},
117-
'cookies': dict(self.request.cookies)}
122+
payload = dict(
123+
headers={k.decode('utf-8'): v.decode('utf-8') for k, v in self.request.headers},
124+
cookies=dict(self.request.cookies),
125+
)
118126
token = generate_jwt_token(session_id,
119127
secret_key=None,
120128
signed=False,
121129
expiration=300,
122130
extra_payload=payload)
123-
session = await self.application_context.create_session_if_needed(session_id, self.request, token)
131+
try:
132+
session = await self.application_context.create_session_if_needed(session_id, self.request, token)
133+
except Exception as e:
134+
log.exception(e)
124135
return session
125136

137+
126138
class AutoloadJsConsumer(SessionConsumer):
127139

128140
async def handle(self, body: bytes) -> None:
@@ -143,7 +155,12 @@ async def handle(self, body: bytes) -> None:
143155

144156
resources_param = self.get_argument("resources", "default")
145157
resources = self.resources(server_url) if resources_param != "none" else None
146-
bundle = bundle_for_objs_and_resources(None, resources)
158+
159+
root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
160+
try:
161+
bundle = bundle_for_objs_and_resources(None, resources, root_url=root_url)
162+
except TypeError:
163+
bundle = bundle_for_objs_and_resources(None, resources)
147164

148165
render_items = [RenderItem(token=session.token, elementid=element_id, use_for_title=False)]
149166
bundle.add(Script(script_for_render_items({}, render_items, app_path=app_path, absolute_url=absolute_url)))
@@ -157,34 +174,42 @@ async def handle(self, body: bytes) -> None:
157174
]
158175
await self.send_response(200, js.encode(), headers=headers)
159176

177+
160178
class DocConsumer(SessionConsumer):
161179

162180
async def handle(self, body: bytes) -> None:
163181
session = await self._get_session()
164-
page = server_html_page_for_session(session,
165-
resources=self.resources(),
166-
title=session.document.title,
167-
template=session.document.template,
168-
template_variables=session.document.template_variables)
182+
page = server_html_page_for_session(
183+
session,
184+
resources=self.resources(),
185+
title=session.document.title,
186+
template=session.document.template,
187+
template_variables=session.document.template_variables
188+
)
169189
await self.send_response(200, page.encode(), headers=[(b"Content-Type", b"text/html")])
170190

191+
171192
class WSConsumer(AsyncWebsocketConsumer, ConsumerHelper):
172193

173194
_clients: Set[ServerConnection]
174195

175-
application_context: ApplicationContext
196+
_application_context: ApplicationContext | None
176197

177-
def __init__(self, scope: Dict[str, Any]) -> None:
178-
super().__init__(scope)
198+
def __init__(self, *args: Any, **kwargs: Any) -> None:
199+
super().__init__(*args, **kwargs)
200+
self._application_context = kwargs.get('app_context')
201+
self._clients = set()
202+
self.lock = locks.Lock()
179203

180-
kwargs = self.scope['url_route']["kwargs"]
181-
self.application_context = kwargs["app_context"]
204+
@property
205+
def application_context(self) -> ApplicationContext:
206+
# backward compatiblity
207+
if self._application_context is None:
208+
self._application_context = self.scope["url_route"]["kwargs"]["app_context"]
182209

183-
if self.application_context.io_loop is None:
210+
if self._application_context.io_loop is None:
184211
raise RuntimeError("io_loop should already been set")
185-
186-
self._clients = set()
187-
self.lock = locks.Lock()
212+
return self._application_context
188213

189214
async def connect(self):
190215
log.info('WebSocket connection opened')
@@ -279,39 +304,44 @@ async def _send_bokeh_message(self, message: Message) -> int:
279304
await self.send(text_data=message.content_json)
280305
sent += len(message.content_json)
281306

282-
for header, payload in message._buffers:
307+
for buffer in message._buffers:
308+
if isinstance(buffer, tuple):
309+
header, payload = buffer
310+
else:
311+
# buffer is bokeh.core.serialization.Buffer (Bokeh 3)
312+
header = {'id': buffer.id}
313+
payload = buffer.data.tobytes()
314+
283315
await self.send(text_data=json.dumps(header))
284316
await self.send(bytes_data=payload)
285317
sent += len(header) + len(payload)
286-
except Exception: # Tornado 4.x may raise StreamClosedError
318+
319+
except Exception as e: # Tornado 4.x may raise StreamClosedError
287320
# on_close() is / will be called anyway
288-
log.warn("Failed sending message as connection was closed")
321+
log.exception(e)
322+
log.warning("Failed sending message as connection was closed")
289323
return sent
290324

325+
async def send_message(self, message: Message) -> int:
326+
return await self._send_bokeh_message(message)
327+
291328
def _new_connection(self,
292329
protocol: Protocol,
293330
socket: AsyncConsumer,
294331
application_context: ApplicationContext,
295332
session: ServerSession) -> ServerConnection:
296-
connection = AsyncServerConnection(protocol, socket, application_context, session)
333+
connection = ServerConnection(protocol, socket, application_context, session)
297334
self._clients.add(connection)
298335
return connection
299336

300-
#-----------------------------------------------------------------------------
337+
# -----------------------------------------------------------------------------
301338
# Dev API
302-
#-----------------------------------------------------------------------------
339+
# -----------------------------------------------------------------------------
303340

304-
#-----------------------------------------------------------------------------
341+
# -----------------------------------------------------------------------------
305342
# Private API
306-
#-----------------------------------------------------------------------------
307-
308-
# TODO: remove this when coroutines are dropped
309-
class AsyncServerConnection(ServerConnection):
343+
# -----------------------------------------------------------------------------
310344

311-
async def send_patch_document(self, event):
312-
""" Sends a PATCH-DOC message, returning a Future that's completed when it's written out. """
313-
msg = self.protocol.create('PATCH-DOC', [event])
314-
await self._socket._send_bokeh_message(msg)
315345

316346
class AttrDict(dict):
317347
""" Provide a dict subclass that supports access by named attributes.
@@ -321,6 +351,6 @@ class AttrDict(dict):
321351
def __getattr__(self, key):
322352
return self[key]
323353

324-
#-----------------------------------------------------------------------------
354+
# -----------------------------------------------------------------------------
325355
# Code
326-
#-----------------------------------------------------------------------------
356+
# -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)