253 lines
6.7 KiB
Python
253 lines
6.7 KiB
Python
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
|
|
from ipython_genutils.py3compat import PY3
|
|
|
|
import pytest
|
|
try:
|
|
from unittest import mock
|
|
except ImportError:
|
|
import mock
|
|
|
|
|
|
from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int, TraitError, observe
|
|
|
|
from .utils import setup, teardown
|
|
|
|
from ..widget import Widget
|
|
|
|
#
|
|
# 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 first the {'value': 2.0} state to be send, followed by the {'value': 42.0} state
|
|
msg = {'method': 'update', 'state': {'value': 2.0}, 'buffer_paths': []}
|
|
call2 = mock.call(msg, buffers=[])
|
|
|
|
msg = {'method': 'update', 'state': {'value': 42.0}, 'buffer_paths': []}
|
|
call42 = mock.call(msg, buffers=[])
|
|
|
|
calls = [call2, call42]
|
|
widget._send.assert_has_calls(calls)
|