From 48a3d5454c58c82f3ba3de485706cfb7f5dfb509 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Mon, 1 Aug 2022 15:34:04 +0200 Subject: [PATCH 1/6] Move comm package --- .github/workflows/tests.yml | 2 +- python/ipywidgets/ipywidgets/__init__.py | 25 +++++++++++++----- .../ipywidgets/ipywidgets/tests/test_embed.py | 3 +++ .../widgets/tests/test_interaction.py | 1 - .../widgets/tests/test_widget_upload.py | 2 +- .../ipywidgets/widgets/tests/utils.py | 26 +++++++++++++++++-- .../ipywidgets/ipywidgets/widgets/widget.py | 19 +++++++++----- .../ipywidgets/widgets/widget_output.py | 8 +++--- python/ipywidgets/setup.cfg | 2 +- 9 files changed, 66 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 37c873e947..e8867ae112 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -131,7 +131,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install file://$PWD/python/ipywidgets#egg=ipywidgets + pip install file://$PWD/python/ipywidgets#egg=ipywidgets[test] - name: Install JS dependencies run: | yarn diff --git a/python/ipywidgets/ipywidgets/__init__.py b/python/ipywidgets/ipywidgets/__init__.py index 4eb6942356..9e40f66034 100644 --- a/python/ipywidgets/ipywidgets/__init__.py +++ b/python/ipywidgets/ipywidgets/__init__.py @@ -21,28 +21,39 @@ from ._version import __version__, __protocol_version__, __jupyter_widgets_controls_version__, __jupyter_widgets_base_version__ 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 .widgets import * -from traitlets import link, dlink + def load_ipython_extension(ip): """Set up Jupyter 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) def _handle_ipython(): """Register with the comm target at import if running in Jupyter""" ip = get_ipython() if ip is None: return - load_ipython_extension(ip) + register_comm_target() _handle_ipython() diff --git a/python/ipywidgets/ipywidgets/tests/test_embed.py b/python/ipywidgets/ipywidgets/tests/test_embed.py index a295442455..83a0788451 100644 --- a/python/ipywidgets/ipywidgets/tests/test_embed.py +++ b/python/ipywidgets/ipywidgets/tests/test_embed.py @@ -9,6 +9,9 @@ import traitlets +# This has a byproduct of setting up the comms +import ipykernel.ipkernel + from ..widgets import IntSlider, IntText, Text, Widget, jslink, HBox, widget_serialization, widget as widget_module from ..embed import embed_data, embed_snippet, embed_minimal_html, dependency_state diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py b/python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py index 0821eb6c20..0dc7e5fcfc 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py @@ -624,4 +624,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) - diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_upload.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_upload.py index f8dc1a8cee..5a06f31b57 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_upload.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_upload.py @@ -76,8 +76,8 @@ def test_serialization_deserialization_integrity(self): from ipykernel.comm import Comm uploader = FileUpload() mock_comm = MagicMock(spec=Comm) - mock_comm.kernel = 'does not matter' mock_comm.send = MagicMock() + mock_comm.kernel = 'does not matter' uploader.comm = mock_comm message = {'value': [FILE_UPLOAD_FRONTEND_CONTENT]} uploader.set_state(message) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/utils.py b/python/ipywidgets/ipywidgets/widgets/tests/utils.py index 7b9b8fcc29..efb572fd56 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/utils.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/utils.py @@ -1,11 +1,14 @@ # 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): +import comm +from ipykernel.comm import Comm + + +class DummyComm(): comm_id = 'a-b-c-d' kernel = 'Truthy' @@ -16,16 +19,33 @@ def __init__(self, *args, **kwargs): 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() +orig_create_comm = comm.create_comm +orig_get_comm_manager = comm.get_comm_manager + def setup_test_comm(): + comm.create_comm = dummy_create_comm + comm.get_comm_manager = dummy_get_comm_manager Widget.comm.klass = DummyComm ipywidgets.widgets.widget.Comm = DummyComm _widget_attrs['_repr_mimebundle_'] = Widget._repr_mimebundle_ @@ -34,6 +54,8 @@ def raise_not_implemented(*args, **kwargs): Widget._repr_mimebundle_ = raise_not_implemented def teardown_test_comm(): + comm.create_comm = orig_create_comm + comm.get_comm_manager = orig_get_comm_manager Widget.comm.klass = Comm ipywidgets.widgets.widget.Comm = Comm for attr, value in _widget_attrs.items(): diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 030fdc45ce..52afa5881c 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -10,9 +10,8 @@ from contextlib import contextmanager from collections.abc import Iterable from IPython 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 json import loads as jsonloads, dumps as jsondumps @@ -480,7 +479,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(None, allow_none=True) keys = List(help="The traits which are synced.") @@ -525,7 +524,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): @@ -686,7 +693,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: # 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 @@ -814,7 +821,7 @@ def _repr_mimebundle_(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: self.comm.send(data=msg, buffers=buffers) def _repr_keys(self): diff --git a/python/ipywidgets/ipywidgets/widgets/widget_output.py b/python/ipywidgets/ipywidgets/widgets/widget_output.py index 6d1bfdae4b..c5c3b072b4 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_output.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_output.py @@ -111,9 +111,9 @@ def __enter__(self): kernel = None if ip and getattr(ip, "kernel", None) is not None: kernel = ip.kernel - elif self.comm is not None and self.comm.kernel is not None: + elif self.comm is not None and getattr(self.comm, "kernel", None) is not None: kernel = self.comm.kernel - + if kernel: parent = None if hasattr(kernel, "get_parent"): @@ -134,7 +134,9 @@ def __exit__(self, etype, evalue, tb): if ip: kernel = ip ip.showtraceback((etype, evalue, tb), tb_offset=0) - elif self.comm is not None and self.comm.kernel is not None: + elif (self.comm is not None and + getattr(self.comm, "kernel", None) is not None and + getattr(self.comm.kernel, "send_response", None) is not None): kernel = self.comm.kernel kernel.send_response(kernel.iopub_socket, u'error', diff --git a/python/ipywidgets/setup.cfg b/python/ipywidgets/setup.cfg index 8d60e3fcf2..0c63d8892e 100644 --- a/python/ipywidgets/setup.cfg +++ b/python/ipywidgets/setup.cfg @@ -34,7 +34,6 @@ zip_safe = False packages = find: install_requires = - ipykernel>=4.5.1 ipython>=6.1.0 traitlets>=4.3.1 widgetsnbextension~=4.0 @@ -43,6 +42,7 @@ install_requires = [options.extras_require] test = jsonschema + ipykernel pytest>=3.6.0 pytest-cov pytz From e78d15168e8bb30eed2035fd704e52f85721249f Mon Sep 17 00:00:00 2001 From: martinRenou Date: Thu, 16 Feb 2023 10:56:39 +0100 Subject: [PATCH 2/6] 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) --- .../ipywidgets/widgets/tests/test_widget_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_templates.py b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_templates.py index e53683630c..449a994278 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_widget_templates.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_widget_templates.py @@ -229,7 +229,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) @@ -240,7 +240,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): From 4e5dd2223b10b4dd0079fc63479e12388aa98fce Mon Sep 17 00:00:00 2001 From: martinRenou Date: Thu, 16 Feb 2023 11:20:36 +0100 Subject: [PATCH 3/6] Fix Python 3.7 test --- .../ipywidgets/widgets/tests/utils.py | 44 ++++++++++++++----- .../ipywidgets/ipywidgets/widgets/widget.py | 8 ++-- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/utils.py b/python/ipywidgets/ipywidgets/widgets/tests/utils.py index efb572fd56..260485e3f8 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/utils.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/utils.py @@ -4,8 +4,14 @@ from ipywidgets import Widget import ipywidgets.widgets.widget -import comm -from ipykernel.comm import 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(): @@ -13,7 +19,7 @@ class DummyComm(): kernel = 'Truthy' def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super().__init__() self.messages = [] def open(self, *args, **kwargs): @@ -40,12 +46,24 @@ def dummy_get_comm_manager(**kwargs): _widget_attrs = {} undefined = object() -orig_create_comm = comm.create_comm -orig_get_comm_manager = comm.get_comm_manager +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(): - comm.create_comm = dummy_create_comm - comm.get_comm_manager = dummy_get_comm_manager + 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['_repr_mimebundle_'] = Widget._repr_mimebundle_ @@ -54,10 +72,14 @@ def raise_not_implemented(*args, **kwargs): Widget._repr_mimebundle_ = raise_not_implemented def teardown_test_comm(): - comm.create_comm = orig_create_comm - comm.get_comm_manager = orig_get_comm_manager - 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) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 52afa5881c..51aedf41f2 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -11,7 +11,7 @@ from collections.abc import Iterable from IPython import get_ipython from traitlets import ( - Any, HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container, + HasTraits, Unicode, Dict, Instance, List, Int, Set, Bytes, observe, default, Container, Undefined) from json import loads as jsonloads, dumps as jsondumps @@ -479,7 +479,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 = Any(None, allow_none=True) + comm = Instance(object, allow_none=True) keys = List(help="The traits which are synced.") @@ -693,7 +693,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: + if self.comm is not None and (self.comm.kernel is not None if hasattr(self.comm, "kernel") else True): # 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 @@ -821,7 +821,7 @@ def _repr_mimebundle_(self, **kwargs): def _send(self, msg, buffers=None): """Sends a message to the model in the front-end.""" - if self.comm 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): From 2a31b38773843b609ca71cb18e587c7c1f31372e Mon Sep 17 00:00:00 2001 From: martinRenou Date: Fri, 17 Mar 2023 09:28:23 +0100 Subject: [PATCH 4/6] Code suggestions Co-authored-by: Maarten Breddels --- python/ipywidgets/ipywidgets/widgets/widget.py | 2 +- python/ipywidgets/ipywidgets/widgets/widget_output.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 51aedf41f2..470c3c35f5 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -693,7 +693,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 hasattr(self.comm, "kernel") else True): + 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 diff --git a/python/ipywidgets/ipywidgets/widgets/widget_output.py b/python/ipywidgets/ipywidgets/widgets/widget_output.py index c5c3b072b4..5fa6aa936c 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_output.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_output.py @@ -111,7 +111,7 @@ def __enter__(self): kernel = None if ip and getattr(ip, "kernel", None) is not None: kernel = ip.kernel - elif self.comm is not None and getattr(self.comm, "kernel", None) is not None: + elif self.comm is not None and getattr(self.comm, 'kernel', True) is not None: kernel = self.comm.kernel if kernel: From 74ee6404430fb4e0ecf9312df8374109480c7894 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Fri, 17 Mar 2023 09:31:05 +0100 Subject: [PATCH 5/6] Add comment --- python/ipywidgets/ipywidgets/widgets/widget_output.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ipywidgets/ipywidgets/widgets/widget_output.py b/python/ipywidgets/ipywidgets/widgets/widget_output.py index 5fa6aa936c..571dace844 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget_output.py +++ b/python/ipywidgets/ipywidgets/widgets/widget_output.py @@ -136,6 +136,7 @@ def __exit__(self, etype, evalue, tb): ip.showtraceback((etype, evalue, tb), tb_offset=0) elif (self.comm is not None and getattr(self.comm, "kernel", None) is not None and + # Check if it's ipykernel getattr(self.comm.kernel, "send_response", None) is not None): kernel = self.comm.kernel kernel.send_response(kernel.iopub_socket, From ad8ec437220b92ee12abd014130ad5eb5fceeba3 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Fri, 17 Mar 2023 09:32:07 +0100 Subject: [PATCH 6/6] Any --- python/ipywidgets/ipywidgets/widgets/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 470c3c35f5..418fd7e9a3 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -11,7 +11,7 @@ from collections.abc import Iterable from IPython import get_ipython 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 json import loads as jsonloads, dumps as jsondumps @@ -479,7 +479,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(object, allow_none=True) + comm = Any(allow_none=True) keys = List(help="The traits which are synced.")