forked from jupyter-widgets/ipywidgets
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for setting JUPYTER_WIDGETS_ECHO to False
This copies the test_set_state file from before jupyter-widgets#3195 to make sure that it still works unchanged when disabling echo_update messages.
- Loading branch information
1 parent
3b2e20d
commit 6cccc9c
Showing
1 changed file
with
279 additions
and
0 deletions.
There are no files selected for viewing
279 changes: 279 additions & 0 deletions
279
python/ipywidgets/ipywidgets/widgets/tests/test_set_state_noecho.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
# Copyright (c) Jupyter Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||
|
||
import pytest | ||
from unittest import mock | ||
|
||
from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int, TraitError, observe | ||
|
||
from .utils import setup, teardown | ||
|
||
import ipywidgets | ||
from ipywidgets import Widget | ||
|
||
# Everything in this file assumes echo is false | ||
@pytest.fixture(autouse=True) | ||
def echo(): | ||
oldvalue = ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO | ||
ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO = False | ||
yield | ||
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).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).tag(sync=True, to_json=bytes_serializer, from_json=truncate_deserializer) | ||
|
||
|
||
# | ||
# Actual tests: | ||
# | ||
|
||
def test_set_state_simple(): | ||
w = SimpleWidget() | ||
w.set_state(dict( | ||
a=True, | ||
b=[True, False, True], | ||
c=[False, True, False], | ||
)) | ||
|
||
assert w.comm.messages == [] | ||
|
||
|
||
def test_set_state_transformer(): | ||
w = TransformerWidget() | ||
w.set_state(dict( | ||
d=[True, False, True] | ||
)) | ||
# Since the deserialize step changes the state, this should send an update | ||
assert w.comm.messages == [((), dict( | ||
buffers=[], | ||
data=dict( | ||
buffer_paths=[], | ||
method='update', | ||
state=dict(d=[False, True, False]) | ||
)))] | ||
|
||
|
||
def test_set_state_data(): | ||
w = DataWidget() | ||
data = memoryview(b'x'*30) | ||
w.set_state(dict( | ||
a=True, | ||
d={'data': data}, | ||
)) | ||
assert w.comm.messages == [] | ||
|
||
|
||
def test_set_state_data_truncate(): | ||
w = TruncateDataWidget() | ||
data = memoryview(b'x'*30) | ||
w.set_state(dict( | ||
a=True, | ||
d={'data': data}, | ||
)) | ||
# Get message for checking | ||
assert len(w.comm.messages) == 1 # ensure we didn't get more than expected | ||
msg = w.comm.messages[0] | ||
# Assert that the data update (truncation) sends an update | ||
buffers = msg[1].pop('buffers') | ||
assert msg == ((), dict( | ||
data=dict( | ||
buffer_paths=[['d', 'data']], | ||
method='update', | ||
state=dict(d={}) | ||
))) | ||
|
||
# Sanity: | ||
assert len(buffers) == 1 | ||
assert buffers[0] == data[:20].tobytes() | ||
|
||
|
||
def test_set_state_numbers_int(): | ||
# JS does not differentiate between float/int. | ||
# Instead, it formats exact floats as ints in JSON (1.0 -> '1'). | ||
|
||
w = NumberWidget() | ||
# Set everything with ints | ||
w.set_state(dict( | ||
f = 1, | ||
cf = 2, | ||
i = 3, | ||
ci = 4, | ||
)) | ||
# Ensure no update message gets produced | ||
assert len(w.comm.messages) == 0 | ||
|
||
|
||
def test_set_state_numbers_float(): | ||
w = NumberWidget() | ||
# Set floats to int-like floats | ||
w.set_state(dict( | ||
f = 1.0, | ||
cf = 2.0, | ||
ci = 4.0 | ||
)) | ||
# Ensure no update message gets produced | ||
assert len(w.comm.messages) == 0 | ||
|
||
|
||
def test_set_state_float_to_float(): | ||
w = NumberWidget() | ||
# Set floats to float | ||
w.set_state(dict( | ||
f = 1.2, | ||
cf = 2.6, | ||
)) | ||
# Ensure no update message gets produced | ||
assert len(w.comm.messages) == 0 | ||
|
||
|
||
def test_set_state_cint_to_float(): | ||
w = NumberWidget() | ||
|
||
# Set CInt to float | ||
w.set_state(dict( | ||
ci = 5.6 | ||
)) | ||
# Ensure an update message gets produced | ||
assert len(w.comm.messages) == 1 | ||
msg = w.comm.messages[0] | ||
data = msg[1]['data'] | ||
assert data['method'] == 'update' | ||
assert data['state'] == {'ci': 5} | ||
|
||
|
||
# This test is disabled, meaning ipywidgets REQUIRES | ||
# any JSON received to format int-like numbers as ints | ||
def _x_test_set_state_int_to_int_like(): | ||
# Note: Setting i to an int-like float will produce an | ||
# error, so if JSON producer were to always create | ||
# float formatted numbers, this would fail! | ||
|
||
w = NumberWidget() | ||
# Set floats to int-like floats | ||
w.set_state(dict( | ||
i = 3.0 | ||
)) | ||
# Ensure no update message gets produced | ||
assert len(w.comm.messages) == 0 | ||
|
||
|
||
def test_set_state_int_to_float(): | ||
w = NumberWidget() | ||
|
||
# Set Int to float | ||
with pytest.raises(TraitError): | ||
w.set_state(dict( | ||
i = 3.5 | ||
)) | ||
|
||
def test_property_lock(): | ||
# when this widget's value is set to 42, it sets itself to 2, and then back to 42 again (and then stops) | ||
class AnnoyingWidget(Widget): | ||
value = Float().tag(sync=True) | ||
stop = Bool(False) | ||
|
||
@observe('value') | ||
def _propagate_value(self, change): | ||
print('_propagate_value', change.new) | ||
if self.stop: | ||
return | ||
if change.new == 42: | ||
self.value = 2 | ||
if change.new == 2: | ||
self.stop = True | ||
self.value = 42 | ||
|
||
widget = AnnoyingWidget(value=1) | ||
assert widget.value == 1 | ||
|
||
widget._send = mock.MagicMock() | ||
# this mimics a value coming from the front end | ||
widget.set_state({'value': 42}) | ||
assert widget.value == 42 | ||
|
||
# we expect no new state to be sent | ||
calls = [] | ||
widget._send.assert_has_calls(calls) | ||
|
||
def test_hold_sync(): | ||
# when this widget's value is set to 42, it sets the value to 2, and also sets a different trait value | ||
class AnnoyingWidget(Widget): | ||
value = Float().tag(sync=True) | ||
other = Float().tag(sync=True) | ||
|
||
@observe('value') | ||
def _propagate_value(self, change): | ||
print('_propagate_value', change.new) | ||
if change.new == 42: | ||
self.value = 2 | ||
self.other = 11 | ||
|
||
widget = AnnoyingWidget(value=1) | ||
assert widget.value == 1 | ||
|
||
widget._send = mock.MagicMock() | ||
# this mimics a value coming from the front end | ||
widget.set_state({'value': 42}) | ||
assert widget.value == 2 | ||
assert widget.other == 11 | ||
|
||
# we expect only single state to be sent, i.e. the {'value': 42.0} state | ||
msg = {'method': 'update', 'state': {'value': 2.0, 'other': 11.0}, 'buffer_paths': []} | ||
call42 = mock.call(msg, buffers=[]) | ||
|
||
calls = [call42] | ||
widget._send.assert_has_calls(calls) |