test_set_state.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. from ipython_genutils.py3compat import PY3
  4. import pytest
  5. try:
  6. from unittest import mock
  7. except ImportError:
  8. import mock
  9. from traitlets import Bool, Tuple, List, Instance, CFloat, CInt, Float, Int, TraitError, observe
  10. from .utils import setup, teardown
  11. from ..widget import Widget
  12. #
  13. # First some widgets to test on:
  14. #
  15. # A widget with simple traits (list + tuple to ensure both are handled)
  16. class SimpleWidget(Widget):
  17. a = Bool().tag(sync=True)
  18. b = Tuple(Bool(), Bool(), Bool(), default_value=(False, False, False)).tag(sync=True)
  19. c = List(Bool()).tag(sync=True)
  20. # A widget with various kinds of number traits
  21. class NumberWidget(Widget):
  22. f = Float().tag(sync=True)
  23. cf = CFloat().tag(sync=True)
  24. i = Int().tag(sync=True)
  25. ci = CInt().tag(sync=True)
  26. # A widget where the data might be changed on reception:
  27. def transform_fromjson(data, widget):
  28. # Switch the two last elements when setting from json, if the first element is True
  29. # and always set first element to False
  30. if not data[0]:
  31. return data
  32. return [False] + data[1:-2] + [data[-1], data[-2]]
  33. class TransformerWidget(Widget):
  34. d = List(Bool()).tag(sync=True, from_json=transform_fromjson)
  35. # A widget that has a buffer:
  36. class DataInstance():
  37. def __init__(self, data=None):
  38. self.data = data
  39. def mview_serializer(instance, widget):
  40. return { 'data': memoryview(instance.data) if instance.data else None }
  41. def bytes_serializer(instance, widget):
  42. return { 'data': bytearray(memoryview(instance.data).tobytes()) if instance.data else None }
  43. def deserializer(json_data, widget):
  44. return DataInstance( memoryview(json_data['data']).tobytes() if json_data else None )
  45. class DataWidget(SimpleWidget):
  46. d = Instance(DataInstance).tag(sync=True, to_json=mview_serializer, from_json=deserializer)
  47. # A widget that has a buffer that might be changed on reception:
  48. def truncate_deserializer(json_data, widget):
  49. return DataInstance( json_data['data'][:20].tobytes() if json_data else None )
  50. class TruncateDataWidget(SimpleWidget):
  51. d = Instance(DataInstance).tag(sync=True, to_json=bytes_serializer, from_json=truncate_deserializer)
  52. #
  53. # Actual tests:
  54. #
  55. def test_set_state_simple():
  56. w = SimpleWidget()
  57. w.set_state(dict(
  58. a=True,
  59. b=[True, False, True],
  60. c=[False, True, False],
  61. ))
  62. assert w.comm.messages == []
  63. def test_set_state_transformer():
  64. w = TransformerWidget()
  65. w.set_state(dict(
  66. d=[True, False, True]
  67. ))
  68. # Since the deserialize step changes the state, this should send an update
  69. assert w.comm.messages == [((), dict(
  70. buffers=[],
  71. data=dict(
  72. buffer_paths=[],
  73. method='update',
  74. state=dict(d=[False, True, False])
  75. )))]
  76. def test_set_state_data():
  77. w = DataWidget()
  78. data = memoryview(b'x'*30)
  79. w.set_state(dict(
  80. a=True,
  81. d={'data': data},
  82. ))
  83. assert w.comm.messages == []
  84. def test_set_state_data_truncate():
  85. w = TruncateDataWidget()
  86. data = memoryview(b'x'*30)
  87. w.set_state(dict(
  88. a=True,
  89. d={'data': data},
  90. ))
  91. # Get message for checking
  92. assert len(w.comm.messages) == 1 # ensure we didn't get more than expected
  93. msg = w.comm.messages[0]
  94. # Assert that the data update (truncation) sends an update
  95. buffers = msg[1].pop('buffers')
  96. assert msg == ((), dict(
  97. data=dict(
  98. buffer_paths=[['d', 'data']],
  99. method='update',
  100. state=dict(d={})
  101. )))
  102. # Sanity:
  103. assert len(buffers) == 1
  104. assert buffers[0] == data[:20].tobytes()
  105. def test_set_state_numbers_int():
  106. # JS does not differentiate between float/int.
  107. # Instead, it formats exact floats as ints in JSON (1.0 -> '1').
  108. w = NumberWidget()
  109. # Set everything with ints
  110. w.set_state(dict(
  111. f = 1,
  112. cf = 2,
  113. i = 3,
  114. ci = 4,
  115. ))
  116. # Ensure no update message gets produced
  117. assert len(w.comm.messages) == 0
  118. def test_set_state_numbers_float():
  119. w = NumberWidget()
  120. # Set floats to int-like floats
  121. w.set_state(dict(
  122. f = 1.0,
  123. cf = 2.0,
  124. ci = 4.0
  125. ))
  126. # Ensure no update message gets produced
  127. assert len(w.comm.messages) == 0
  128. def test_set_state_float_to_float():
  129. w = NumberWidget()
  130. # Set floats to float
  131. w.set_state(dict(
  132. f = 1.2,
  133. cf = 2.6,
  134. ))
  135. # Ensure no update message gets produced
  136. assert len(w.comm.messages) == 0
  137. def test_set_state_cint_to_float():
  138. w = NumberWidget()
  139. # Set CInt to float
  140. w.set_state(dict(
  141. ci = 5.6
  142. ))
  143. # Ensure an update message gets produced
  144. assert len(w.comm.messages) == 1
  145. msg = w.comm.messages[0]
  146. data = msg[1]['data']
  147. assert data['method'] == 'update'
  148. assert data['state'] == {'ci': 5}
  149. # This test is disabled, meaning ipywidgets REQUIRES
  150. # any JSON received to format int-like numbers as ints
  151. def _x_test_set_state_int_to_int_like():
  152. # Note: Setting i to an int-like float will produce an
  153. # error, so if JSON producer were to always create
  154. # float formatted numbers, this would fail!
  155. w = NumberWidget()
  156. # Set floats to int-like floats
  157. w.set_state(dict(
  158. i = 3.0
  159. ))
  160. # Ensure no update message gets produced
  161. assert len(w.comm.messages) == 0
  162. def test_set_state_int_to_float():
  163. w = NumberWidget()
  164. # Set Int to float
  165. with pytest.raises(TraitError):
  166. w.set_state(dict(
  167. i = 3.5
  168. ))
  169. def test_property_lock():
  170. # when this widget's value is set to 42, it sets itself to 2, and then back to 42 again (and then stops)
  171. class AnnoyingWidget(Widget):
  172. value = Float().tag(sync=True)
  173. stop = Bool(False)
  174. @observe('value')
  175. def _propagate_value(self, change):
  176. print('_propagate_value', change.new)
  177. if self.stop:
  178. return
  179. if change.new == 42:
  180. self.value = 2
  181. if change.new == 2:
  182. self.stop = True
  183. self.value = 42
  184. widget = AnnoyingWidget(value=1)
  185. assert widget.value == 1
  186. widget._send = mock.MagicMock()
  187. # this mimics a value coming from the front end
  188. widget.set_state({'value': 42})
  189. assert widget.value == 42
  190. # we expect first the {'value': 2.0} state to be send, followed by the {'value': 42.0} state
  191. msg = {'method': 'update', 'state': {'value': 2.0}, 'buffer_paths': []}
  192. call2 = mock.call(msg, buffers=[])
  193. msg = {'method': 'update', 'state': {'value': 42.0}, 'buffer_paths': []}
  194. call42 = mock.call(msg, buffers=[])
  195. calls = [call2, call42]
  196. widget._send.assert_has_calls(calls)