Skip to content

Commit

Permalink
jupyter_bokeh 3.0.6 (bokeh 3 and ipywidgets 8) (#178)
Browse files Browse the repository at this point in the history
* v3.0.5, added support for ipywidgets>=8.0.0 (#169)

* v3.0.5, added support for ipywidgets>=8.0.0

* Update nodejs in CI

Co-authored-by: Lev Maximov <lev.maximov@gmail.com>
Co-authored-by: Mateusz Paprocki <mattpap@gmail.com>

* Update yarn.lock

* npm audit fix

* Upgrade webpack and webpack-cli

* Migrate the build to webpack 5

* Add index.js.LICENSE.txt

* Update and unify dependencies

* Update yarn.lock

* Bump version to 3.0.6

* Upgrade actions and nodejs in CI

* Adjust version spec scheme

* Upgrade jupyter-packaging

* Add types to jupyter_bokeh/widgets.py

* Remove unused imports

* Rename trigger_{json_->}event()

* Update event handling to Bokeh 3.0

* Update .gitignore

---------

Co-authored-by: Lev Maximov <axil.github@gmail.com>
Co-authored-by: Lev Maximov <lev.maximov@gmail.com>
  • Loading branch information
3 people authored Mar 6, 2023
1 parent f343b90 commit 4f7f661
Show file tree
Hide file tree
Showing 10 changed files with 7,757 additions and 11,626 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: '16.x'
node-version: '18.x'
- name: Install Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.7'
python-version: '3.8'
architecture: 'x64'
- name: Setup pip cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: pip-3.7-${{ hashFiles('package.json') }}
key: pip-3.8-${{ hashFiles('package.json') }}
restore-keys: |
pip-3.7-
pip-3.8-
pip-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Setup yarn cache
uses: actions/cache@v2
uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
Expand All @@ -43,7 +43,7 @@ jobs:
yarn-
- name: Install dependencies
run: python -m pip install -U jupyterlab~=3.0 jupyter_packaging~=0.7.9
run: python -m pip install -U jupyterlab~=3.0 jupyter_packaging~=0.12.3
- name: Build the extension
run: |
jlpm
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/jupyter_bokeh/labextension/
/jupyter_bokeh/nbextension/index.js
/jupyter_bokeh/nbextension/index.js.map
__pycache__/
8 changes: 4 additions & 4 deletions conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ requirements:
- notebook
- python
- setuptools
- nodejs >=10.13.0
- nodejs >=18.0
run:
- python
- bokeh >=2.0.0
- ipywidgets >=7.5.0
- bokeh 2.4.*,3.*
- ipywidgets 8.*
run_constrained:
- jupyterlab >=3.0.0,<4
- jupyterlab 3.*

test:
imports:
Expand Down
39 changes: 39 additions & 0 deletions jupyter_bokeh/nbextension/index.js.LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2021-02-16
*/

/*!
* jQuery JavaScript Library v3.6.0
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2021-03-02T17:08Z
*/

/*! *****************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
110 changes: 72 additions & 38 deletions jupyter_bokeh/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

# Standard library imports
import json
from typing import TYPE_CHECKING, Any, TypedDict

# External imports
from ipywidgets import DOMWidget
Expand All @@ -26,13 +28,18 @@
from bokeh.document import Document
from bokeh.embed.elements import div_for_render_item
from bokeh.embed.util import standalone_docs_json_and_render_items
from bokeh.events import Event
from bokeh.models import LayoutDOM
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.protocol import Protocol
from bokeh.util.dependencies import import_optional
from bokeh.core.serialization import Deserializer, Serialized
from bokeh.model import Model

from ._version import __version__

if TYPE_CHECKING:
from bokeh.core.types import ID
from bokeh.document.events import DocumentPatchedEvent
from bokeh.document.json import DocJson

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------
Expand All @@ -48,6 +55,12 @@
# General API
#-----------------------------------------------------------------------------

class RenderBundle(TypedDict):

docs_json: dict[ID, DocJson]
render_items: list[dict[str, Any]] # TODO: list[RenderItemJson]
div: str

class BokehModel(DOMWidget):

_model_name = Unicode("BokehModel").tag(sync=True)
Expand All @@ -61,79 +74,100 @@ class BokehModel(DOMWidget):
combine_events = Bool(False).tag(sync=True)
render_bundle = Dict().tag(sync=True, to_json=lambda obj, _: serialize_json(obj))

_model: Model

@property
def _document(self):
def _document(self) -> Document | None:
return self._model.document

def __init__(self, model, **kwargs):
def __init__(self, model: LayoutDOM, **kwargs: Any) -> None:
assert isinstance(model, LayoutDOM)
self.update_from_model(model)
super(BokehModel, self).__init__(**kwargs)
self.on_msg(self._sync_model)

def close(self):
def close(self) -> None:
super().close()
if self._document is not None:
self._document.remove_on_change(self)

@classmethod
def _model_to_traits(cls, model):
def _model_to_traits(cls, model: Model) -> RenderBundle:
if model.document is None:
document = Document()
document.add_root(model)
(docs_json, [render_item]) = standalone_docs_json_and_render_items([model], suppress_callback_warning=True)
render_bundle = dict(
render_bundle = RenderBundle(
docs_json=docs_json,
render_items=[render_item.to_json()],
div=div_for_render_item(render_item),
)
return render_bundle

def update_from_model(self, model):
def update_from_model(self, model: Model) -> None:
self._model = model
self.render_bundle = self._model_to_traits(model)
self._document.on_change_dispatch_to(self)

def _document_patched(self, event):
def _document_patched(self, event: DocumentPatchedEvent) -> None:
if event.setter is self:
return
msg = Protocol().create("PATCH-DOC", [event])

self.send({"msg": "patch", "payload": msg.header_json})
self.send({"msg": "patch", "payload": msg.metadata_json})
self.send({"msg": "patch", "payload": msg.content_json})
for header, buffer in msg.buffers:
self.send({"msg": "patch", "payload": json.dumps(header)})
self.send({"msg": "patch"}, [buffer])
for buffer in msg.buffers:
header = json.dumps(buffer.ref)
payload = buffer.to_bytes()
self.send({"msg": "patch", "payload": header})
self.send({"msg": "patch"}, [payload])

def _sync_model(self, _, content, _buffers):
def _sync_model(self, _model: BokehModel, content: dict[str, Any], _buffers: list[Any]) -> None:
if content.get("event", "") != "jsevent":
return
kind = content.get("kind")
if kind == 'ModelChanged':
hint = content.get("hint")
if hint:
cds = self._model.select_one({"id": hint["column_source"]["id"]})
if "patches" in hint:
# Handle ColumnsPatchedEvent
cds.patch(hint["patches"], setter=self)
elif "data" in hint:
# Handle ColumnsStreamedEvent
cds._stream(hint["data"], rollover=hint["rollover"], setter=self)
return

# Handle ModelChangedEvent
new, old, attr = content["new"], content["old"], content["attr"]
submodel = self._model.select_one({"id": content["id"]})
descriptor = submodel.lookup(content['attr'])
try:
descriptor._set(submodel, old, new, hint=hint, setter=self)
except Exception:
return
for cb in submodel._callbacks.get(attr, []):
del content["event"]

setter: Any = self

assert self._document is not None
deserializer = Deserializer(list(self._document.models), setter=setter)
event = deserializer.deserialize(Serialized(content=content, buffers=[]))

kind = event["kind"]
if kind == "ModelChanged":
attr = event["attr"]
model = event["model"]
new = event["new"]

assert isinstance(model, Model)
descriptor = model.lookup(attr)

# descriptor.set_from_json()
new = descriptor.property.prepare_value(model, descriptor.name, new)
old = descriptor._get(model)
descriptor._set(model, old, new, setter=setter)

for cb in model._callbacks.get(attr, []):
cb(attr, old, new)
elif kind == 'MessageSent':
self._document.callbacks.trigger_json_event(content["msg_data"])
elif kind == "ColumnsStreamed":
model = content["model"]
data = content["data"]
rollover = content["rollover"]

assert isinstance(model, ColumnDataSource)
model._stream(data, rollover, setter=setter)
elif kind == "ColumnsPatched":
model = content["model"]
patches = content["data"]

assert isinstance(model, ColumnDataSource)
model.patch(patches, setter=setter)
elif kind == "MessageSent":
msg_type = event["msg_type"]
msg_data = event["msg_data"]
if msg_type == "bokeh_event":
self._document.callbacks.trigger_event(msg_data)

#-----------------------------------------------------------------------------
# Dev API
Expand Down
Loading

0 comments on commit 4f7f661

Please sign in to comment.