From d1e5bb83a3251da6a04ac0b9e8d536c6ca2529c9 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Tue, 21 Mar 2023 09:46:00 +0100 Subject: [PATCH] fix: allow use of comm package (#3533) * 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 (cherry picked from commit 785d1598b38f6fb43917489f530d14e28876fc24) --- ipywidgets/__init__.py | 25 ++++++--- ipywidgets/tests/test_embed.py | 5 +- ipywidgets/widgets/tests/test_interaction.py | 1 - .../widgets/tests/test_widget_templates.py | 4 +- ipywidgets/widgets/tests/utils.py | 54 +++++++++++++++++-- ipywidgets/widgets/widget.py | 18 +++++-- ipywidgets/widgets/widget_output.py | 2 +- setup.py | 3 +- 8 files changed, 88 insertions(+), 24 deletions(-) diff --git a/ipywidgets/__init__.py b/ipywidgets/__init__.py index 8ea8fd723d..e3359b9f5f 100644 --- a/ipywidgets/__init__.py +++ b/ipywidgets/__init__.py @@ -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 @@ -48,6 +59,6 @@ def _handle_ipython(): ip = get_ipython() if ip is None: return - load_ipython_extension(ip) + register_comm_target() _handle_ipython() diff --git a/ipywidgets/tests/test_embed.py b/ipywidgets/tests/test_embed.py index 1240404818..73a34bd00b 100644 --- a/ipywidgets/tests/test_embed.py +++ b/ipywidgets/tests/test_embed.py @@ -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: diff --git a/ipywidgets/widgets/tests/test_interaction.py b/ipywidgets/widgets/tests/test_interaction.py index 9614e45646..c403b2dca5 100644 --- a/ipywidgets/widgets/tests/test_interaction.py +++ b/ipywidgets/widgets/tests/test_interaction.py @@ -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) - diff --git a/ipywidgets/widgets/tests/test_widget_templates.py b/ipywidgets/widgets/tests/test_widget_templates.py index 9cbc4ad5d8..49f7617c14 100644 --- a/ipywidgets/widgets/tests/test_widget_templates.py +++ b/ipywidgets/widgets/tests/test_widget_templates.py @@ -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) @@ -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): diff --git a/ipywidgets/widgets/tests/utils.py b/ipywidgets/widgets/tests/utils.py index 49bf1b41cf..86163a5a28 100644 --- a/ipywidgets/widgets/tests/utils.py +++ b/ipywidgets/widgets/tests/utils.py @@ -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_ @@ -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) diff --git a/ipywidgets/widgets/widget.py b/ipywidgets/widgets/widget.py index c8756da551..bf88db3a64 100644 --- a/ipywidgets/widgets/widget.py +++ b/ipywidgets/widgets/widget.py @@ -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 @@ -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.") @@ -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): @@ -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 @@ -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): diff --git a/ipywidgets/widgets/widget_output.py b/ipywidgets/widgets/widget_output.py index f02b93bae4..8050b77845 100644 --- a/ipywidgets/widgets/widget_output.py +++ b/ipywidgets/widgets/widget_output.py @@ -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. diff --git a/setup.py b/setup.py index 9709e5e4ce..386b6fb8cd 100644 --- a/setup.py +++ b/setup.py @@ -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 @@ -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: