Skip to content

Commit

Permalink
fix: allow use of comm package (jupyter-widgets#3533)
Browse files Browse the repository at this point in the history
* Move comm package

* Fix wrongly written test

This test was supposed to fail, as setting the bottom_left value will
trigger a ipywidgets.Layout.send_state on `child.layout.grid_area = position`
(ipywidgets.widgets.widget_template.py L419) and on
`self.layout.grid_template_areas = grid_template_areas_css`
(ipywidgets.widgets.widget_template.py L448)

* Fix Python 3.7 test

Co-authored-by: Maarten Breddels <maartenbreddels@gmail.com>
(cherry picked from commit 785d159)
  • Loading branch information
martinRenou committed Mar 21, 2023
1 parent 5f2e97a commit d1e5bb8
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 24 deletions.
25 changes: 18 additions & 7 deletions ipywidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,36 @@

import os

from traitlets import link, dlink
from IPython import get_ipython

try:
from comm import get_comm_manager
except ImportError:
def get_comm_manager():
ip = get_ipython()

if ip is not None and ip.kernel is not None:
return get_ipython().kernel.comm_manager

from ._version import version_info, __version__, __protocol_version__, __jupyter_widgets_controls_version__, __jupyter_widgets_base_version__
from .widgets import *
from traitlets import link, dlink



def load_ipython_extension(ip):
"""Set up IPython to work with widgets"""
if not hasattr(ip, 'kernel'):
return
register_comm_target(ip.kernel)
register_comm_target()


def register_comm_target(kernel=None):
"""Register the jupyter.widget comm target"""
if kernel is None:
kernel = get_ipython().kernel
kernel.comm_manager.register_target('jupyter.widget', Widget.handle_comm_opened)
kernel.comm_manager.register_target('jupyter.widget.control', Widget.handle_control_comm_opened)
comm_manager = get_comm_manager()

comm_manager.register_target('jupyter.widget', Widget.handle_comm_opened)
comm_manager.register_target('jupyter.widget.control', Widget.handle_control_comm_opened)

# deprecated alias
handle_kernel = register_comm_target
Expand All @@ -48,6 +59,6 @@ def _handle_ipython():
ip = get_ipython()
if ip is None:
return
load_ipython_extension(ip)
register_comm_target()

_handle_ipython()
5 changes: 4 additions & 1 deletion ipywidgets/tests/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

import traitlets

from ..widgets import IntSlider, IntText, Text, Widget, jslink, HBox, widget_serialization
# This has a byproduct of setting up the comms
import ipykernel.ipkernel

from ..widgets import IntSlider, IntText, Text, Widget, jslink, HBox, widget_serialization, widget
from ..embed import embed_data, embed_snippet, embed_minimal_html, dependency_state

try:
Expand Down
1 change: 0 additions & 1 deletion ipywidgets/widgets/tests/test_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,4 +734,3 @@ def test_state_schema():
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../', 'state.schema.json')) as f:
schema = json.load(f)
jsonschema.validate(state, schema)

4 changes: 2 additions & 2 deletions ipywidgets/widgets/tests/test_widget_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_update_dynamically(self, send_state): #pylint: disable=no-self-use
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
# check whether frontend was informed
send_state.assert_called_once_with(key="grid_template_areas")
send_state.assert_called_with(key="grid_template_areas")

box = widgets.TwoByTwoLayout(top_left=button1, top_right=button3,
bottom_left=None, bottom_right=button4)
Expand All @@ -244,7 +244,7 @@ def test_update_dynamically(self, send_state): #pylint: disable=no-self-use
box.merge = False
assert box.layout.grid_template_areas == ('"top-left top-right"\n' +
'"bottom-left bottom-right"')
send_state.assert_called_once_with(key="grid_template_areas")
send_state.assert_called_with(key="grid_template_areas")


class TestAppLayout(TestCase):
Expand Down
54 changes: 49 additions & 5 deletions ipywidgets/widgets/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,69 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from ipykernel.comm import Comm
from ipywidgets import Widget
import ipywidgets.widgets.widget

class DummyComm(Comm):
# The new comm package is not available in our Python 3.7 CI (older ipykernel version)
try:
import comm
NEW_COMM_PACKAGE = True
except ImportError:
NEW_COMM_PACKAGE = False

import ipykernel.comm


class DummyComm():
comm_id = 'a-b-c-d'
kernel = 'Truthy'

def __init__(self, *args, **kwargs):
super(DummyComm, self).__init__(*args, **kwargs)
super().__init__()
self.messages = []

def open(self, *args, **kwargs):
pass

def on_msg(self, *args, **kwargs):
pass

def send(self, *args, **kwargs):
self.messages.append((args, kwargs))

def close(self, *args, **kwargs):
pass


def dummy_create_comm(**kwargs):
return DummyComm()


def dummy_get_comm_manager(**kwargs):
return {}


_widget_attrs = {}
undefined = object()

if NEW_COMM_PACKAGE:
orig_comm = ipykernel.comm.comm.BaseComm
else:
orig_comm = ipykernel.comm.Comm
orig_create_comm = None
orig_get_comm_manager = None

if NEW_COMM_PACKAGE:
orig_create_comm = comm.create_comm
orig_get_comm_manager = comm.get_comm_manager

def setup_test_comm():
if NEW_COMM_PACKAGE:
comm.create_comm = dummy_create_comm
comm.get_comm_manager = dummy_get_comm_manager
ipykernel.comm.comm.BaseComm = DummyComm
else:
ipykernel.comm.Comm = DummyComm
Widget.comm.klass = DummyComm
ipywidgets.widgets.widget.Comm = DummyComm
_widget_attrs['_ipython_display_'] = Widget._ipython_display_
Expand All @@ -34,8 +72,14 @@ def raise_not_implemented(*args, **kwargs):
Widget._ipython_display_ = raise_not_implemented

def teardown_test_comm():
Widget.comm.klass = Comm
ipywidgets.widgets.widget.Comm = Comm
if NEW_COMM_PACKAGE:
comm.create_comm = orig_create_comm
comm.get_comm_manager = orig_get_comm_manager
ipykernel.comm.comm.BaseComm = orig_comm
else:
ipykernel.comm.Comm = orig_comm
Widget.comm.klass = orig_comm
ipywidgets.widgets.widget.Comm = orig_comm
for attr, value in _widget_attrs.items():
if value is undefined:
delattr(Widget, attr)
Expand Down
18 changes: 13 additions & 5 deletions ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from IPython.core.getipython import get_ipython
from ipykernel.comm import Comm
from traitlets import (
HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container,
Any, HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container,
Undefined)
from ipython_genutils.py3compat import string_types, PY3
from IPython.display import display
Expand Down Expand Up @@ -454,7 +454,7 @@ def get_view_spec(self):

_view_count = Int(None, allow_none=True,
help="EXPERIMENTAL: The number of views of the model displayed in the frontend. This attribute is experimental and may change or be removed in the future. None signifies that views will not be tracked. Set this to 0 to start tracking view creation/deletion.").tag(sync=True)
comm = Instance('ipykernel.comm.Comm', allow_none=True)
comm = Any(allow_none=True)

keys = List(help="The traits which are synced.")

Expand Down Expand Up @@ -500,7 +500,15 @@ def open(self):
if self._model_id is not None:
args['comm_id'] = self._model_id

self.comm = Comm(**args)
try:
from comm import create_comm
except ImportError:
def create_comm(**kwargs):
from ipykernel.comm import Comm

return Comm(**kwargs)

self.comm = create_comm(**args)

@observe('comm')
def _comm_changed(self, change):
Expand Down Expand Up @@ -678,7 +686,7 @@ def notify_change(self, change):
# Send the state to the frontend before the user-registered callbacks
# are called.
name = change['name']
if self.comm is not None and self.comm.kernel is not None:
if self.comm is not None and getattr(self.comm, 'kernel', True) is not None:
# Make sure this isn't information that the front-end just sent us.
if name in self.keys and self._should_send_property(name, getattr(self, name)):
# Send new state to front-end
Expand Down Expand Up @@ -813,7 +821,7 @@ def _ipython_display_(self, **kwargs):

def _send(self, msg, buffers=None):
"""Sends a message to the model in the front-end."""
if self.comm is not None and self.comm.kernel is not None:
if self.comm is not None and (self.comm.kernel is not None if hasattr(self.comm, "kernel") else True):
self.comm.send(data=msg, buffers=buffers)

def _repr_keys(self):
Expand Down
2 changes: 1 addition & 1 deletion ipywidgets/widgets/widget_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Output(DOMWidget):
context will be captured and displayed in the widget instead of the standard output
area.
You can also use the .capture() method to decorate a function or a method. Any output
You can also use the .capture() method to decorate a function or a method. Any output
produced by the function will then go to the output widget. This is useful for
debugging widget callbacks, for example.
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@

setuptools_args = {}
install_requires = setuptools_args['install_requires'] = [
'ipykernel>=4.5.1',
'ipython_genutils~=0.2.0',
'traitlets>=4.3.1',
# TODO: Dynamically add this dependency
Expand All @@ -125,7 +124,7 @@
':python_version>="3.3"': ['ipython>=4.0.0'],
':python_version>="3.6"': ['jupyterlab_widgets>=1.0.0,<3'],
'test:python_version=="2.7"': ['mock'],
'test': ['pytest>=3.6.0', 'pytest-cov'],
'test': ['pytest>=3.6.0', 'pytest-cov', 'ipykernel'],
}

if 'setuptools' in sys.modules:
Expand Down

0 comments on commit d1e5bb8

Please sign in to comment.