reacton 1.8.1__tar.gz → 1.8.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {reacton-1.8.1 → reacton-1.8.3}/PKG-INFO +1 -1
  2. reacton-1.8.3/reacton/_version.py +1 -0
  3. {reacton-1.8.1 → reacton-1.8.3}/reacton/core.py +84 -45
  4. {reacton-1.8.1 → reacton-1.8.3}/reacton/core_test.py +39 -12
  5. reacton-1.8.1/reacton/_version.py +0 -1
  6. {reacton-1.8.1 → reacton-1.8.3}/.gitignore +0 -0
  7. {reacton-1.8.1 → reacton-1.8.3}/LICENSE +0 -0
  8. {reacton-1.8.1 → reacton-1.8.3}/pyproject.toml +0 -0
  9. {reacton-1.8.1 → reacton-1.8.3}/react_ipywidgets/__init__.py +0 -0
  10. {reacton-1.8.1 → reacton-1.8.3}/react_ipywidgets/core.py +0 -0
  11. {reacton-1.8.1 → reacton-1.8.3}/reacton/__init__.py +0 -0
  12. {reacton-1.8.1 → reacton-1.8.3}/reacton/bqplot.py +0 -0
  13. {reacton-1.8.1 → reacton-1.8.3}/reacton/deprecated/__init__.py +0 -0
  14. {reacton-1.8.1 → reacton-1.8.3}/reacton/deprecated/ipyvuetify.py +0 -0
  15. {reacton-1.8.1 → reacton-1.8.3}/reacton/find.py +0 -0
  16. {reacton-1.8.1 → reacton-1.8.3}/reacton/find_test.py +0 -0
  17. {reacton-1.8.1 → reacton-1.8.3}/reacton/generate.py +0 -0
  18. {reacton-1.8.1 → reacton-1.8.3}/reacton/generate_test.py +0 -0
  19. {reacton-1.8.1 → reacton-1.8.3}/reacton/ipycanvas.py +0 -0
  20. {reacton-1.8.1 → reacton-1.8.3}/reacton/ipyvue.py +0 -0
  21. {reacton-1.8.1 → reacton-1.8.3}/reacton/ipyvuetify.py +0 -0
  22. {reacton-1.8.1 → reacton-1.8.3}/reacton/ipywidgets.py +0 -0
  23. {reacton-1.8.1 → reacton-1.8.3}/reacton/logging.py +0 -0
  24. {reacton-1.8.1 → reacton-1.8.3}/reacton/patch.py +0 -0
  25. {reacton-1.8.1 → reacton-1.8.3}/reacton/patch_display.py +0 -0
  26. {reacton-1.8.1 → reacton-1.8.3}/reacton/py.typed +0 -0
  27. {reacton-1.8.1 → reacton-1.8.3}/reacton/rx.py +0 -0
  28. {reacton-1.8.1 → reacton-1.8.3}/reacton/test.vue +0 -0
  29. {reacton-1.8.1 → reacton-1.8.3}/reacton/test_rx.py +0 -0
  30. {reacton-1.8.1 → reacton-1.8.3}/reacton/utils.py +0 -0
  31. {reacton-1.8.1 → reacton-1.8.3}/reacton/utils_test.py +0 -0
  32. {reacton-1.8.1 → reacton-1.8.3}/reacton/work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reacton
3
- Version: 1.8.1
3
+ Version: 1.8.3
4
4
  Project-URL: Home, https://www.github.com/widgetti/reacton
5
5
  Project-URL: Documentation, https://reacton.solara.dev
6
6
  Project-URL: Source code, https://www.github.com/widgetti/reacton
@@ -0,0 +1 @@
1
+ __version__ = "1.8.3"
@@ -770,14 +770,42 @@ def get_widget(el: Element):
770
770
 
771
771
 
772
772
  def use_state(initial: T, key: str = None, eq: Callable[[Any, Any], bool] = None) -> Tuple[T, Callable[[Union[T, Callable[[T], T]]], None]]:
773
- """Returns a (value, setter) tuple that is used to manage state in a component.
773
+ """Returns a `(value, setter)` tuple that is used to manage state in a component.
774
774
 
775
775
  This function can only be called from a component function.
776
776
 
777
- The value returns the current state (which equals initial at the first render call).
778
- Or the value that was last
777
+ The value returns the current state (which equals `initial` at the first render call). Or the value that was last set using the setter.
778
+
779
+ Note that the setter function can be used in two ways.
780
+
781
+ Directly setting the value:
782
+
783
+ ```python
784
+ @reacton.component
785
+ def ButtonClick():
786
+ clicks, set_clicks = reacton.use_state(0)
787
+ def my_click_handler():
788
+ set_clicks(clicks+1)
789
+ button = w.Button(description=f"Clicked {clicks} times",
790
+ on_click=my_click_handler)
791
+ return button
792
+ ```
793
+
794
+ Updating the value based on the previous/current value.
795
+
796
+ ```python
797
+ @reacton.component
798
+ def ButtonClick():
799
+ clicks, set_clicks = reacton.use_state(0)
800
+ def my_click_handler():
801
+ set_clicks(lambda prev: prev+1)
802
+ button = w.Button(description=f"Clicked {clicks} times",
803
+ on_click=my_click_handler)
804
+ return button
805
+ ```
806
+
807
+ The last one avoid issues with stale data, which means you have a reference to the value of an old render pass (not present in this simple example).
779
808
 
780
- Subsequent
781
809
  """
782
810
  rc = _get_render_context()
783
811
  return rc.use_state(initial, key, eq)
@@ -839,7 +867,7 @@ def use_reducer(reduce: Callable[[T, U], T], initial_state: T) -> Tuple[T, Calla
839
867
  return state, dispatch
840
868
 
841
869
 
842
- def use_memo(f, dependencies=None, debug_name: str = None):
870
+ def use_memo(f: Callable[[], T], dependencies=None, debug_name: str = None) -> T:
843
871
  if debug_name is None:
844
872
  debug_name = f.__name__
845
873
  rc = _get_render_context()
@@ -1217,15 +1245,15 @@ class _RenderContext:
1217
1245
  memo = (value, dependencies)
1218
1246
  self.context.memo.append(memo)
1219
1247
  self.context.memo_index += 1
1220
- logger.info("Initial memo = %r for index %r (debug-name: %r)", memo, self.context.memo_index - 1, name)
1248
+ logger.debug("Initial memo = %r for index %r (debug-name: %r)", memo, self.context.memo_index - 1, name)
1221
1249
  return value
1222
1250
  else:
1223
1251
  memo = self.context.memo[self.context.memo_index]
1224
1252
  value, dependencies_previous = memo
1225
1253
  if utils.equals(dependencies_previous, dependencies):
1226
- logger.info("Got memo hit = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name)
1254
+ logger.debug("Got memo hit = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name)
1227
1255
  else:
1228
- logger.info("Replace memo with = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name)
1256
+ logger.debug("Replace memo with = %r for index %r (debug-name: %r)", memo, self.context.memo_index, name)
1229
1257
  value = f()
1230
1258
  memo = (value, dependencies)
1231
1259
  self.context.memo[self.context.memo_index] = memo
@@ -1243,11 +1271,11 @@ class _RenderContext:
1243
1271
  self.context.state_metadata[key] = len(initial)
1244
1272
  elif utils.isinstance_lazy(initial, "pandas.DataFrame"):
1245
1273
  self.context.state_metadata[key] = utils.dataframe_fingerprint(initial)
1246
- logger.info("Initial state = %r for key %r (%r)", initial, key, id(self.context))
1274
+ logger.debug("Initial state = %r for key %r (%r)", initial, key, id(self.context))
1247
1275
  return initial, self.make_setter(key, self.context, eq)
1248
1276
  else:
1249
1277
  state = self.context.state[key]
1250
- logger.info("Got state = %r for key %r (%r)", state, key, id(self.context))
1278
+ logger.debug("Got state = %r for key %r (%r)", state, key, id(self.context))
1251
1279
  return state, self.make_setter(key, self.context, eq)
1252
1280
 
1253
1281
  def make_setter(self, key, context: ComponentContext, eq: Callable[[Any, Any], bool] = None):
@@ -1258,16 +1286,8 @@ class _RenderContext:
1258
1286
  # I am not sure exactly why, but this will at least make
1259
1287
  # it easier for Python to garbage collect, since it will
1260
1288
  # avoid circular references
1261
- context_ref = weakref.ref(context)
1262
- self_ref = weakref.ref(self)
1263
- del context
1264
- del self
1265
1289
 
1266
1290
  def set_(value):
1267
- context = context_ref()
1268
- assert context is not None
1269
- self = self_ref()
1270
- assert self is not None
1271
1291
  if callable(value):
1272
1292
  value = value(context.state[key])
1273
1293
  logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
@@ -1344,12 +1364,12 @@ class _RenderContext:
1344
1364
  if len(self.context.effects) <= self.context.effect_index:
1345
1365
  self.context.effect_index += 1
1346
1366
  self.context.effects.append(Effect(effect, dependencies))
1347
- logger.info("Initial effect = %r for index %r (%r)", effect, self.context.effect_index - 1, dependencies)
1367
+ logger.debug("Initial effect = %r for index %r (%r)", effect, self.context.effect_index - 1, dependencies)
1348
1368
  else:
1349
1369
  previous_effect = self.context.effects[self.context.effect_index]
1350
1370
  # we always set it, even replacing it when we didn't execute it
1351
1371
  # in the consolidation phase we decide what to do (e.g. skip it)
1352
- logger.info("Setting next effect = %r for index %r (%r)", effect, self.context.effect_index, dependencies)
1372
+ logger.debug("Setting next effect = %r for index %r (%r)", effect, self.context.effect_index, dependencies)
1353
1373
  if previous_effect.executed:
1354
1374
  # line up...
1355
1375
  previous_effect.next = Effect(effect, dependencies)
@@ -1494,7 +1514,11 @@ class _RenderContext:
1494
1514
  "make sure your root component always returns the same component type"
1495
1515
  )
1496
1516
  if container:
1497
- container.children = [widget]
1517
+ if widget is None:
1518
+ # Exception occurred, and we cannot render the widget
1519
+ container.children = []
1520
+ else:
1521
+ container.children = [widget]
1498
1522
 
1499
1523
  if self.context_root.exceptions_children or self.context_root.exceptions_self:
1500
1524
  # an exception bubbled up during reconsolidate
@@ -1852,7 +1876,7 @@ class _RenderContext:
1852
1876
  # if we have a next, it means that effect itself is executed
1853
1877
  # TODO: custom equals
1854
1878
  if effect.next.dependencies is not None and utils.equals(effect.dependencies, effect.next.dependencies):
1855
- logger.info("No need to add effect, dependencies are the same (%r %r)", effect.callable, effect.dependencies)
1879
+ logger.debug("No need to add effect, dependencies are the same (%r %r)", effect.callable, effect.dependencies)
1856
1880
  # not needed, just remove the reference
1857
1881
  effect.next = None
1858
1882
  else:
@@ -1936,34 +1960,48 @@ class _RenderContext:
1936
1960
  raise RuntimeError(f"Element ({el}) was already in self._shared_widgets")
1937
1961
  else:
1938
1962
  logger.info("Creating new widget: %r %r", el, key)
1939
- widget, orphan_ids = el._create_widget(kwargs)
1940
-
1963
+ widget = None
1964
+ if not context.exceptions_children:
1965
+ try:
1966
+ widget, orphan_ids = el._create_widget(kwargs)
1967
+ except BaseException as e:
1968
+ context.exceptions_self.append(e)
1969
+ self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (creating widget)"))
1970
+ self._rerender_needed = True
1941
1971
  if el.is_shared:
1942
1972
  self._shared_widgets[el] = widget
1943
1973
  else:
1944
1974
  context.widgets[key] = widget
1945
1975
  elif el_prev is not None and el_prev.component == el.component:
1946
- logger.info("Updating widget: %r → %r %r", el_prev, el, key)
1976
+ logger.debug("Updating widget: %r → %r %r", el_prev, el, key)
1947
1977
  assert el_prev is not None
1948
1978
  # TODO: remove event listeners while doing so
1949
1979
  # assign to _widgets[el] first, before errors can occur
1950
1980
  kwargs = reconsolidate_children()
1951
- try:
1952
- el._update_widget(widget_previous, el_prev, kwargs)
1953
- except BaseException as e:
1954
- context.exceptions_self.append(e)
1955
- self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)"))
1956
- self._rerender_needed = True
1981
+ if not context.exceptions_children:
1982
+ try:
1983
+ el._update_widget(widget_previous, el_prev, kwargs)
1984
+ except BaseException as e:
1985
+ context.exceptions_self.append(e)
1986
+ self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)"))
1987
+ self._rerender_needed = True
1957
1988
  if el.is_shared:
1958
1989
  self._shared_widgets[el] = widget_previous
1959
1990
  else:
1960
1991
  context.widgets[key] = widget_previous
1961
1992
  else:
1962
1993
  assert el_prev is not None, "widget_previous is not None, but el_prev is"
1963
- logger.info("Replacing widget: %r → %r %r", el_prev, el, key)
1994
+ logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
1964
1995
  self._remove_element(el_prev, key, parent_key=parent_key)
1965
1996
  kwargs = reconsolidate_children()
1966
- widget, orphan_ids = el._create_widget(kwargs)
1997
+ widget = None
1998
+ if not context.exceptions_children:
1999
+ try:
2000
+ widget, orphan_ids = el._create_widget(kwargs)
2001
+ except BaseException as e:
2002
+ context.exceptions_self.append(e)
2003
+ self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during reconciliation (updating widget)"))
2004
+ self._rerender_needed = True
1967
2005
  if el.is_shared:
1968
2006
  self._shared_widgets[el] = widget
1969
2007
  else:
@@ -2050,7 +2088,7 @@ class _RenderContext:
2050
2088
  assert key is not None
2051
2089
  assert self.context is not None
2052
2090
  context = self.context
2053
- logger.info("Remove: (%s, %s) %r", parent_key, key, el)
2091
+ logger.debug("Remove: (%s, %s) %r", parent_key, key, el)
2054
2092
 
2055
2093
  if el.is_shared:
2056
2094
  if el not in self._shared_elements:
@@ -2098,17 +2136,18 @@ class _RenderContext:
2098
2136
  widget = self._shared_widgets[el]
2099
2137
  else:
2100
2138
  widget = context.widgets[key]
2101
- assert widget.comm is not None
2102
- assert widget.model_id in _get_widgets_dict()
2103
-
2104
- for orphan in self._orphans.get(widget.model_id, set()):
2105
- orphan_widget = _get_widgets_dict().get(orphan)
2106
- if orphan_widget:
2107
- close_widget(orphan_widget)
2108
- if widget.model_id in self._orphans:
2109
- del self._orphans[widget.model_id]
2110
- el._cleanup_callbacks(widget)
2111
- el._close_widget(widget)
2139
+ if widget is not None:
2140
+ assert widget.comm is not None
2141
+ assert widget.model_id in _get_widgets_dict()
2142
+
2143
+ for orphan in self._orphans.get(widget.model_id, set()):
2144
+ orphan_widget = _get_widgets_dict().get(orphan)
2145
+ if orphan_widget:
2146
+ close_widget(orphan_widget)
2147
+ if widget.model_id in self._orphans:
2148
+ del self._orphans[widget.model_id]
2149
+ el._cleanup_callbacks(widget)
2150
+ el._close_widget(widget)
2112
2151
  if el.is_shared:
2113
2152
  del self._shared_widgets[el]
2114
2153
  else:
@@ -1913,10 +1913,8 @@ def test_recover_exception_in_cleanup_child(Container1, Container2):
1913
1913
  with pytest.raises(Exception):
1914
1914
  rc.render(Container1(children=[Container2()]))
1915
1915
 
1916
- try:
1916
+ with pytest.raises(Exception, match=".*fail.*"):
1917
1917
  rc.close()
1918
- except Exception as e:
1919
- assert e.args[0] == "fail"
1920
1918
 
1921
1919
 
1922
1920
  def test_recover_exception_in_cleanup():
@@ -1957,25 +1955,46 @@ def test_recover_exception_in_cleanup():
1957
1955
  rc.close()
1958
1956
 
1959
1957
 
1960
- def test_recover_exception_in_widget_update():
1958
+ def test_recover_exception_in_widget_update(Container1, Container2):
1961
1959
  set_fail = None
1962
1960
 
1961
+ @react.component
1962
+ def Child(value):
1963
+ with v.Container() as main:
1964
+ v.Btn(children=[value])
1965
+ return main
1966
+
1963
1967
  @react.component
1964
1968
  def Test():
1965
1969
  nonlocal set_fail
1966
1970
  fail, set_fail = react.use_state(False)
1967
- value = 1
1971
+ value = "ok"
1968
1972
  if fail:
1969
- value = "fail" # type: ignore
1970
- return w.IntSlider(value=value)
1973
+ value = 1 # type: ignore
1974
+ if not fail:
1975
+ with Container1() as main:
1976
+ v.Btn(children=[value])
1977
+ else:
1978
+ with Container2() as main:
1979
+ Child(value=value)
1980
+ return main
1971
1981
 
1972
- # with pytest.raises(Exception):
1973
1982
  box, rc = react.render(Test())
1974
- assert rc.find(ipywidgets.IntSlider)
1983
+ assert rc.find(ipyvuetify.Btn)
1975
1984
  assert set_fail is not None
1976
1985
  set_fail(True)
1977
1986
  assert not rc._is_rendering
1978
- assert not rc.find(ipywidgets.IntSlider)
1987
+ assert not rc.find(ipyvuetify.Btn)
1988
+ rc.close()
1989
+
1990
+ box, rc = react.render(Test(), handle_error=False)
1991
+ assert rc.find(ipyvuetify.Btn)
1992
+ assert set_fail is not None
1993
+ with pytest.raises(Exception, match=".*Could not create widget.*"):
1994
+ set_fail(True)
1995
+ set_fail(True)
1996
+ assert not rc._is_rendering
1997
+ rc.render(w.HTML(value="recover"))
1979
1998
  rc.close()
1980
1999
 
1981
2000
 
@@ -3103,10 +3122,18 @@ def test_event_multiple():
3103
3122
  rc.close()
3104
3123
 
3105
3124
 
3106
- def test_memory_leak(ButtonComponent):
3125
+ @reacton.component
3126
+ def UseMemoComponent():
3127
+ rc = core.get_render_context()
3128
+ reacton.use_memo(lambda: [rc], [])
3129
+ return w.Button()
3130
+
3131
+
3132
+ @pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent])
3133
+ def test_memory_leak(component):
3107
3134
  # we make sure that there is not reference to the context
3108
3135
  # after we close it
3109
- box, rc = react.render(ButtonComponent(), handle_error=False)
3136
+ box, rc = react.render(component(), handle_error=False)
3110
3137
  rc.close()
3111
3138
  # useful for debugging
3112
3139
  # import objgraph
@@ -1 +0,0 @@
1
- __version__ = "1.8.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes