From dd45d41aa47d36651a2b51f65d2ea99b85eb2466 Mon Sep 17 00:00:00 2001 From: Boyuan Deng Date: Thu, 14 Sep 2023 21:49:16 -0400 Subject: [PATCH 1/2] Working version with some tests. --- .../widgets/tests/test_send_state.py | 51 +++++++++++---- .../widgets/tests/test_set_state.py | 65 +------------------ .../ipywidgets/widgets/tests/utils.py | 58 ++++++++++++++++- .../ipywidgets/ipywidgets/widgets/widget.py | 7 ++ 4 files changed, 106 insertions(+), 75 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py b/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py index ec18ae4af1..721192401a 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py @@ -1,22 +1,12 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -from traitlets import Bool, Tuple, List - -from .utils import setup, teardown, DummyComm +from .utils import setup, teardown, DummyComm, SimpleWidget, NumberWidget from ..widget import Widget from ..._version import __control_protocol_version__ -# A widget with simple traits -class SimpleWidget(Widget): - a = Bool().tag(sync=True) - b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag( - sync=True - ) - c = List(Bool()).tag(sync=True) - def test_empty_send_state(): w = SimpleWidget() @@ -29,3 +19,42 @@ def test_empty_hold_sync(): with w.hold_sync(): pass assert w.comm.messages == [] + +def test_control(): + comm = DummyComm() + Widget.close_all() + w = SimpleWidget() + Widget.handle_control_comm_opened( + comm, dict(metadata={'version': __control_protocol_version__}) + ) + Widget._handle_control_comm_msg(dict(content=dict( + data={'method': 'request_states'} + ))) + assert comm.messages + +def test_control_filter(): + comm = DummyComm() + random_widget = SimpleWidget() + random_widget.open() + random_widget_id = random_widget.model_id + important_widget = NumberWidget() + important_widget.open() + important_widget_id = important_widget.model_id + Widget.handle_control_comm_opened( + comm, dict(metadata={'version': __control_protocol_version__}) + ) + Widget._handle_control_comm_msg(dict(content=dict( + data={'method': 'request_states', 'widget_id': important_widget_id} + ))) + # comm.messages have very complicated nested structure, we just want to verify correct widget is included + assert important_widget_id in str(comm.messages[0]) + # And widget not supposed to be there is filtered off + assert random_widget_id not in str(comm.messages[0]) + + # Negative case (should contain all states) + Widget._handle_control_comm_msg(dict(content=dict( + data={'method': 'request_states'} + ))) + assert important_widget_id in str(comm.messages[1]) + assert random_widget_id in str(comm.messages[1]) + diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_set_state.py b/python/ipywidgets/ipywidgets/widgets/tests/test_set_state.py index 82ecbd9311..0a36a3c0ea 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_set_state.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_set_state.py @@ -4,9 +4,9 @@ import pytest from unittest import mock -from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int, TraitError, observe +from traitlets import Bool, Float, TraitError, observe -from .utils import setup, teardown +from .utils import setup, teardown, SimpleWidget, NumberWidget, TransformerWidget, DataWidget, TruncateDataWidget import ipywidgets from ipywidgets import Widget @@ -19,67 +19,6 @@ def echo(request): yield request.param ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO = oldvalue -# -# First some widgets to test on: -# - -# A widget with simple traits (list + tuple to ensure both are handled) -class SimpleWidget(Widget): - a = Bool().tag(sync=True) - b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag(sync=True) - c = List(Bool()).tag(sync=True) - - -# A widget with various kinds of number traits -class NumberWidget(Widget): - f = Float().tag(sync=True) - cf = CFloat().tag(sync=True) - i = Int().tag(sync=True) - ci = CInt().tag(sync=True) - - - -# A widget where the data might be changed on reception: -def transform_fromjson(data, widget): - # Switch the two last elements when setting from json, if the first element is True - # and always set first element to False - if not data[0]: - return data - return [False] + data[1:-2] + [data[-1], data[-2]] - -class TransformerWidget(Widget): - d = List(Bool()).tag(sync=True, from_json=transform_fromjson) - - - -# A widget that has a buffer: -class DataInstance(): - def __init__(self, data=None): - self.data = data - -def mview_serializer(instance, widget): - return { 'data': memoryview(instance.data) if instance.data else None } - -def bytes_serializer(instance, widget): - return { 'data': bytearray(memoryview(instance.data).tobytes()) if instance.data else None } - -def deserializer(json_data, widget): - return DataInstance( memoryview(json_data['data']).tobytes() if json_data else None ) - -class DataWidget(SimpleWidget): - d = Instance(DataInstance, args=()).tag(sync=True, to_json=mview_serializer, from_json=deserializer) - -# A widget that has a buffer that might be changed on reception: -def truncate_deserializer(json_data, widget): - return DataInstance( json_data['data'][:20].tobytes() if json_data else None ) - -class TruncateDataWidget(SimpleWidget): - d = Instance(DataInstance, args=()).tag(sync=True, to_json=bytes_serializer, from_json=truncate_deserializer) - - -# -# Actual tests: -# def test_set_state_simple(echo): w = SimpleWidget() diff --git a/python/ipywidgets/ipywidgets/widgets/tests/utils.py b/python/ipywidgets/ipywidgets/widgets/tests/utils.py index 260485e3f8..7862ecb4d1 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/utils.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/utils.py @@ -2,7 +2,10 @@ # Distributed under the terms of the Modified BSD License. from ipywidgets import Widget +from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int + import ipywidgets.widgets.widget +import uuid # The new comm package is not available in our Python 3.7 CI (older ipykernel version) try: @@ -15,11 +18,11 @@ class DummyComm(): - comm_id = 'a-b-c-d' kernel = 'Truthy' def __init__(self, *args, **kwargs): super().__init__() + self.comm_id = uuid.uuid4().hex self.messages = [] def open(self, *args, **kwargs): @@ -95,3 +98,56 @@ def teardown(): def call_method(method, *args, **kwargs): method(*args, **kwargs) + + +# A widget with simple traits (list + tuple to ensure both are handled) +class SimpleWidget(Widget): + a = Bool().tag(sync=True) + b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag(sync=True) + c = List(Bool()).tag(sync=True) + + +# A widget with various kinds of number traits +class NumberWidget(Widget): + f = Float().tag(sync=True) + cf = CFloat().tag(sync=True) + i = Int().tag(sync=True) + ci = CInt().tag(sync=True) + + + +# A widget where the data might be changed on reception: +def transform_fromjson(data, widget): + # Switch the two last elements when setting from json, if the first element is True + # and always set first element to False + if not data[0]: + return data + return [False] + data[1:-2] + [data[-1], data[-2]] + +class TransformerWidget(Widget): + d = List(Bool()).tag(sync=True, from_json=transform_fromjson) + + +# A widget that has a buffer: +class DataInstance(): + def __init__(self, data=None): + self.data = data + +def mview_serializer(instance, widget): + return { 'data': memoryview(instance.data) if instance.data else None } + +def bytes_serializer(instance, widget): + return { 'data': bytearray(memoryview(instance.data).tobytes()) if instance.data else None } + +def deserializer(json_data, widget): + return DataInstance( memoryview(json_data['data']).tobytes() if json_data else None ) + +class DataWidget(SimpleWidget): + d = Instance(DataInstance, args=()).tag(sync=True, to_json=mview_serializer, from_json=deserializer) + +# A widget that has a buffer that might be changed on reception: +def truncate_deserializer(json_data, widget): + return DataInstance( json_data['data'][:20].tobytes() if json_data else None ) + +class TruncateDataWidget(SimpleWidget): + d = Instance(DataInstance, args=()).tag(sync=True, to_json=bytes_serializer, from_json=truncate_deserializer) diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index 2dc674097d..d29e177446 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -399,6 +399,10 @@ def _handle_control_comm_msg(cls, msg): 'model_module_version': widget._model_module_version, 'state': widget.get_state(drop_defaults=drop_defaults), } + if 'widget_id' in data: + # In this case, we only want 1 widget state + id = data['widget_id'] + full_state = {k: v for k, v in full_state.items() if k == id} full_state, buffer_paths, buffers = _remove_buffers(full_state) cls._control_comm.send(dict( method='update_states', @@ -441,7 +445,9 @@ def get_manager_state(drop_defaults=False, widgets=None): state = {} if widgets is None: widgets = _instances.values() + print(widgets) for widget in widgets: + print(widget.model_id) state[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) return {'version_major': 2, 'version_minor': 0, 'state': state} @@ -533,6 +539,7 @@ def open(self): args['comm_id'] = self._model_id self.comm = comm.create_comm(**args) + print(self.comm) @observe('comm') def _comm_changed(self, change): From ebd632aaf8f6907f31f2be6101fd23b3379d876b Mon Sep 17 00:00:00 2001 From: Boyuan Deng Date: Thu, 14 Sep 2023 21:56:22 -0400 Subject: [PATCH 2/2] Remove some print statements and add another case. --- .../ipywidgets/ipywidgets/widgets/tests/test_send_state.py | 7 +++++++ python/ipywidgets/ipywidgets/widgets/widget.py | 3 --- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py b/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py index 721192401a..cf1e416c4b 100644 --- a/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py +++ b/python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py @@ -58,3 +58,10 @@ def test_control_filter(): assert important_widget_id in str(comm.messages[1]) assert random_widget_id in str(comm.messages[1]) + # Invalid case (widget either already closed or does not exist) + Widget._handle_control_comm_msg(dict(content=dict( + data={'method': 'request_states', 'widget_id': 'no_such_widget'} + ))) + # Should not contain any iPyWidget information in the states + assert not comm.messages[2][0][0]['states'] + diff --git a/python/ipywidgets/ipywidgets/widgets/widget.py b/python/ipywidgets/ipywidgets/widgets/widget.py index d29e177446..aa0841dc05 100644 --- a/python/ipywidgets/ipywidgets/widgets/widget.py +++ b/python/ipywidgets/ipywidgets/widgets/widget.py @@ -445,9 +445,7 @@ def get_manager_state(drop_defaults=False, widgets=None): state = {} if widgets is None: widgets = _instances.values() - print(widgets) for widget in widgets: - print(widget.model_id) state[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) return {'version_major': 2, 'version_minor': 0, 'state': state} @@ -539,7 +537,6 @@ def open(self): args['comm_id'] = self._model_id self.comm = comm.create_comm(**args) - print(self.comm) @observe('comm') def _comm_changed(self, change):