Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "widget_id" filter to "request_states" message, fixing + refactoring some CI #3833

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions python/ipywidgets/ipywidgets/widgets/tests/test_send_state.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -29,3 +19,49 @@ 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])

# 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']

65 changes: 2 additions & 63 deletions python/ipywidgets/ipywidgets/widgets/tests/test_set_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
58 changes: 57 additions & 1 deletion python/ipywidgets/ipywidgets/widgets/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down