Skip to content

Commit

Permalink
Add tests for setting JUPYTER_WIDGETS_ECHO to False
Browse files Browse the repository at this point in the history
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
jasongrout committed Mar 4, 2022
1 parent 3b2e20d commit 6cccc9c
Showing 1 changed file with 279 additions and 0 deletions.
279 changes: 279 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/tests/test_set_state_noecho.py
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)

0 comments on commit 6cccc9c

Please sign in to comment.