reacton 1.1.1__tar.gz → 1.2.1__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 (30) hide show
  1. {reacton-1.1.1 → reacton-1.2.1}/PKG-INFO +1 -1
  2. reacton-1.2.1/reacton/_version.py +1 -0
  3. {reacton-1.1.1 → reacton-1.2.1}/reacton/core.py +81 -6
  4. {reacton-1.1.1 → reacton-1.2.1}/reacton/core_test.py +79 -0
  5. {reacton-1.1.1 → reacton-1.2.1}/reacton/find.py +5 -2
  6. {reacton-1.1.1 → reacton-1.2.1}/reacton/generate.py +4 -4
  7. {reacton-1.1.1 → reacton-1.2.1}/reacton/patch.py +3 -3
  8. reacton-1.2.1/reacton/patch_display.py +45 -0
  9. {reacton-1.1.1 → reacton-1.2.1}/reacton/utils.py +1 -1
  10. {reacton-1.1.1 → reacton-1.2.1}/reacton/utils_test.py +5 -5
  11. reacton-1.1.1/reacton/_version.py +0 -1
  12. {reacton-1.1.1 → reacton-1.2.1}/.gitignore +0 -0
  13. {reacton-1.1.1 → reacton-1.2.1}/LICENSE +0 -0
  14. {reacton-1.1.1 → reacton-1.2.1}/pyproject.toml +0 -0
  15. {reacton-1.1.1 → reacton-1.2.1}/react_ipywidgets/__init__.py +0 -0
  16. {reacton-1.1.1 → reacton-1.2.1}/react_ipywidgets/core.py +0 -0
  17. {reacton-1.1.1 → reacton-1.2.1}/reacton/__init__.py +0 -0
  18. {reacton-1.1.1 → reacton-1.2.1}/reacton/bqplot.py +0 -0
  19. {reacton-1.1.1 → reacton-1.2.1}/reacton/find_test.py +0 -0
  20. {reacton-1.1.1 → reacton-1.2.1}/reacton/generate_test.py +0 -0
  21. {reacton-1.1.1 → reacton-1.2.1}/reacton/ipycanvas.py +0 -0
  22. {reacton-1.1.1 → reacton-1.2.1}/reacton/ipyvue.py +0 -0
  23. {reacton-1.1.1 → reacton-1.2.1}/reacton/ipyvuetify.py +0 -0
  24. {reacton-1.1.1 → reacton-1.2.1}/reacton/ipywidgets.py +0 -0
  25. {reacton-1.1.1 → reacton-1.2.1}/reacton/logging.py +0 -0
  26. {reacton-1.1.1 → reacton-1.2.1}/reacton/py.typed +0 -0
  27. {reacton-1.1.1 → reacton-1.2.1}/reacton/rx.py +0 -0
  28. {reacton-1.1.1 → reacton-1.2.1}/reacton/test.vue +0 -0
  29. {reacton-1.1.1 → reacton-1.2.1}/reacton/test_rx.py +0 -0
  30. {reacton-1.1.1 → reacton-1.2.1}/reacton/work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reacton
3
- Version: 1.1.1
3
+ Version: 1.2.1
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.2.1"
@@ -37,10 +37,12 @@ from warnings import warn
37
37
 
38
38
  import ipywidgets
39
39
  import ipywidgets as widgets
40
+ import traitlets
40
41
  import typing_extensions
41
42
  from typing_extensions import Literal, Protocol
42
43
 
43
44
  import reacton.logging # noqa: F401 # has sidefx
45
+ import reacton.patch_display # noqa: F401 # has sidefx
44
46
 
45
47
  from . import _version, patch, utils # noqa: F401
46
48
 
@@ -529,6 +531,70 @@ class ComponentFunction(Component):
529
531
  self.value_name = value_name
530
532
  functools.update_wrapper(self, f)
531
533
 
534
+ def widget(self, **kwargs):
535
+ return self.widget_class()(**kwargs)
536
+
537
+ def widget_class(self):
538
+ parameters = inspect.signature(self.f).parameters
539
+ args = list(parameters.keys())
540
+ listeners = set()
541
+ normal_kwargs = set()
542
+ for name in args:
543
+ if name.startswith("on_"):
544
+ listeners.add(name)
545
+ else:
546
+ normal_kwargs.add(name)
547
+ component = self
548
+
549
+ class Container(widgets.VBox, widgets.ValueWidget):
550
+ description = traitlets.Unicode()
551
+
552
+ def __init__(self, **kwargs):
553
+ super().__init__(**kwargs)
554
+ self._children = []
555
+ traits = {}
556
+
557
+ for name in normal_kwargs:
558
+ if name != "children": # children is a special trait we keep
559
+ if name in normal_kwargs:
560
+ trait = traitlets.Any(default_value=parameters[name].default)
561
+ else:
562
+ trait = traitlets.Any()
563
+ traits[name] = trait
564
+ Container.add_traits(self, **traits)
565
+ for name, value in kwargs.items():
566
+ if name in normal_kwargs:
567
+ setattr(self, name, value)
568
+ for name in normal_kwargs:
569
+ self.observe(self._rerender, name)
570
+ el = self._create_el()
571
+ self.rc = _RenderContext(el, self)
572
+ self.rc.render(el, self)
573
+
574
+ def _create_el(self):
575
+ kwargs = {}
576
+ for name in normal_kwargs:
577
+ if name != "children":
578
+ kwargs[name] = getattr(self, name)
579
+ for name in listeners:
580
+ propname = name[3:]
581
+
582
+ def closure(propname=propname):
583
+ def setter(value):
584
+ setattr(self, propname, value)
585
+
586
+ return setter
587
+
588
+ kwargs[name] = closure()
589
+ logger.info("calling component with kwargs: %r", kwargs)
590
+ return component(**kwargs)
591
+
592
+ def _rerender(self, change):
593
+ el = self._create_el()
594
+ self.rc.update(el)
595
+
596
+ return Container
597
+
532
598
  def __eq__(self, rhs):
533
599
  if self is rhs:
534
600
  return True
@@ -1115,6 +1181,14 @@ class _RenderContext:
1115
1181
  self.context.effects[self.context.effect_index] = Effect(effect, dependencies)
1116
1182
  self.context.effect_index += 1
1117
1183
 
1184
+ def update(self, element: Element):
1185
+ if self._is_rendering:
1186
+ self.element = element
1187
+ self._rerender_needed = True
1188
+ self._rerender_needed_reason = "root element changed"
1189
+ else:
1190
+ self.render(element, self.container)
1191
+
1118
1192
  def render(self, element: Element, container: widgets.Widget = None):
1119
1193
  # render + consolidate
1120
1194
  widget = None
@@ -1136,11 +1210,12 @@ class _RenderContext:
1136
1210
  try:
1137
1211
  local.rc = self
1138
1212
  self.element = element
1213
+ del element
1139
1214
  main_render_phase = not self._is_rendering
1140
1215
  render_count = self.render_count # make a copy
1141
1216
  self._rerender_needed = False
1142
1217
  self._rerender_needed_reason = None
1143
- logger.info("Render phase: %r %r", self.render_count, "main" if main_render_phase else "(nested)")
1218
+ logger.info("Render phase: %r %r of %r", self.render_count, "main" if main_render_phase else "(nested)", self.element)
1144
1219
  self.render_count += 1
1145
1220
  self._is_rendering = True
1146
1221
  # if we got called recursively, self.context is not the root context
@@ -1149,12 +1224,12 @@ class _RenderContext:
1149
1224
  self.context.exception_handler = False
1150
1225
  self.context.exceptions_children = []
1151
1226
  self.context.exceptions_self = []
1152
- self.context.root_element_next = element
1227
+ self.context.root_element_next = self.element
1153
1228
  assert self.context is not None
1154
1229
 
1155
1230
  try:
1156
1231
  self._shared_elements_next = set()
1157
- self._render(element, "/", parent_key=ROOT_KEY)
1232
+ self._render(self.element, "/", parent_key=ROOT_KEY)
1158
1233
  self.first_render = False
1159
1234
  except BaseException:
1160
1235
  self._is_rendering = False
@@ -1177,7 +1252,7 @@ class _RenderContext:
1177
1252
  self.context.exceptions_children = []
1178
1253
  self.context.exceptions_self = []
1179
1254
 
1180
- self._render(element, "/", parent_key=ROOT_KEY)
1255
+ self._render(self.element, "/", parent_key=ROOT_KEY)
1181
1256
  logger.info("Render done: %r %r", self._rerender_needed, self._rerender_needed_reason)
1182
1257
  assert self.context is self.context_root
1183
1258
  render_counts += 1
@@ -1193,7 +1268,7 @@ class _RenderContext:
1193
1268
  break
1194
1269
 
1195
1270
  logger.info("Render reconsolidate...")
1196
- widget = self._reconsolidate(element, default_key="/", parent_key=ROOT_KEY)
1271
+ widget = self._reconsolidate(self.element, default_key="/", parent_key=ROOT_KEY)
1197
1272
  logger.info("Render reconsolidate done")
1198
1273
  self.context.root_element = self.context.root_element_next
1199
1274
  self.context.root_element_next = None
@@ -1205,7 +1280,7 @@ class _RenderContext:
1205
1280
  logger.debug("\t%r %x", el, id(el))
1206
1281
  # RESET
1207
1282
  assert self.context is self.context_root
1208
- if element.is_shared:
1283
+ if self.element.is_shared:
1209
1284
  assert widget in self._shared_widgets.values()
1210
1285
  else:
1211
1286
  assert widget in self.context_root.widgets.values()
@@ -11,6 +11,7 @@ import numpy as np
11
11
  import pandas as pd
12
12
  import pytest
13
13
  import traitlets
14
+ from IPython.display import display
14
15
 
15
16
  import reacton as react
16
17
  import reacton.core
@@ -2719,3 +2720,81 @@ def test_implicit_component():
2719
2720
  assert rc.find(widgets.Button)[0].widget.description == "hoeba: 1"
2720
2721
  assert rc.find(widgets.Button)[1].widget.description == "hoeba: 2"
2721
2722
  rc.close()
2723
+
2724
+
2725
+ def test_create_widget():
2726
+ @react.component
2727
+ def Test(a: int = 1, label="lala", on_a=None, children=[]):
2728
+ # adding children shouldn't change the children trait
2729
+ assert children == []
2730
+ return w.IntSlider(value=a, on_value=on_a, description=label)
2731
+
2732
+ widget: widgets.Widget = Test.widget(a=1) # type: ignore
2733
+ assert widget.trait_metadata("children", "to_json") is not None
2734
+ mock = unittest.mock.Mock()
2735
+ widget.trait_names() == ["a", "label"]
2736
+ widget.observe(mock, "a")
2737
+ assert widget.label == "lala"
2738
+ widget.a = 2
2739
+ mock.assert_called_once_with({"name": "a", "old": 1, "new": 2, "owner": widget, "type": "change"})
2740
+ slider = widget.children[0]
2741
+ assert isinstance(slider, widgets.IntSlider)
2742
+ slider.value = 3
2743
+ mock.assert_called_with({"name": "a", "old": 2, "new": 3, "owner": widget, "type": "change"})
2744
+
2745
+ widget.rc.close()
2746
+
2747
+ @reacton.component
2748
+ def BiSlider(value, on_value):
2749
+ x, y = value
2750
+ with w.VBox() as main:
2751
+ w.IntSlider(min=0, max=10, value=x, on_value=lambda x: on_value((x, y)))
2752
+ w.IntSlider(min=0, max=x**2, value=y, on_value=lambda y: on_value((x, y)))
2753
+ return main
2754
+
2755
+ widget: widgets.Widget = BiSlider.widget(value=(3, 4)) # type: ignore
2756
+ slider1 = widget.children[0].children[0]
2757
+ slider2 = widget.children[0].children[1]
2758
+ assert slider1.value == 3
2759
+ assert slider2.value == 4
2760
+ widget.value = (2, 3)
2761
+ assert slider1.value == 2
2762
+ assert slider2.value == 3
2763
+
2764
+ slider1.value = 3
2765
+ assert slider2.max == 9
2766
+ widget.rc.close()
2767
+
2768
+
2769
+ def test_display_element():
2770
+ button = w.Button(description="hoeba")
2771
+ from IPython.core.interactiveshell import InteractiveShell
2772
+
2773
+ store = InteractiveShell.instance().display_pub.publish
2774
+ InteractiveShell.instance().display_pub.publish = unittest.mock.Mock()
2775
+
2776
+ @react.component
2777
+ def Test():
2778
+ with w.VBox() as box:
2779
+ display(button)
2780
+ return box
2781
+
2782
+ box, rc = react.render(Test(), handle_error=False)
2783
+ assert rc.find(widgets.Button).widget.description == "hoeba"
2784
+ InteractiveShell.instance().display_pub.publish.assert_not_called()
2785
+ rc.close()
2786
+ InteractiveShell.instance().display_pub.publish = store
2787
+
2788
+
2789
+ def test_display_dataframe():
2790
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
2791
+
2792
+ @react.component
2793
+ def Test():
2794
+ with w.VBox() as box:
2795
+ display(df)
2796
+ return box
2797
+
2798
+ box, rc = react.render(Test(), handle_error=False)
2799
+ # assert rc.find(widgets.Button).widget.description == "hoeba"
2800
+ rc.close()
@@ -50,6 +50,9 @@ class Finder(collections.abc.Sequence, Generic[W]):
50
50
  parent = parent.parent
51
51
  return parent
52
52
 
53
+ def __repr__(self):
54
+ return "Finder structure:\n%r" % self._current_structure()
55
+
53
56
  def _reexecute_find(self):
54
57
  finder = self._root()
55
58
  for query in self.queries:
@@ -86,7 +89,7 @@ class Finder(collections.abc.Sequence, Generic[W]):
86
89
  def matches(self, **matches):
87
90
  self.assert_matches(**matches)
88
91
 
89
- def wait_for(self, state="attached", timeout=1):
92
+ def wait_for(self, state="attached", timeout=1, iteration_delay=0.01):
90
93
  start = time.time()
91
94
  finder = self
92
95
  while time.time() - start < timeout:
@@ -98,7 +101,7 @@ class Finder(collections.abc.Sequence, Generic[W]):
98
101
  return finder
99
102
  else:
100
103
  raise ValueError(f"Unknown state {state}, expected 'attached' or 'detached'")
101
- time.sleep(0.001)
104
+ time.sleep(iteration_delay)
102
105
  finder = self._reexecute_find()
103
106
  raise TimeoutError(f"Timeout waiting for {self}, current structure:\n{self._current_structure()}")
104
107
 
@@ -173,16 +173,16 @@ class CodeGen(Generic[W]):
173
173
  # TODO: we can special case this (Tuple is subclass of Instance)
174
174
  # but it's fixed length
175
175
  if isinstance(trait, traitlets.traitlets.Instance):
176
- if trait.klass.__name__ == "array":
176
+ if trait.klass.__name__ == "array": # type: ignore
177
177
  import pdb
178
178
 
179
179
  pdb.set_trace()
180
180
  if trait.klass.__module__ == "builtins":
181
- return trait.klass.__name__
181
+ return trait.klass.__name__ # type: ignore
182
182
  else:
183
- type_name = str(trait.klass.__module__) + "." + trait.klass.__name__
183
+ type_name = str(trait.klass.__module__) + "." + trait.klass.__name__ # type: ignore
184
184
  type_name = type_name_alias.get(type_name, type_name)
185
- if issubclass(trait.klass, widgets.Widget):
185
+ if issubclass(trait.klass, widgets.Widget): # type: ignore
186
186
  element_class_name_type = "Element" # self.get_element_class
187
187
  type_name = f"{element_class_name_type}[{type_name}]"
188
188
  return type_name
@@ -13,8 +13,8 @@ class Callable(traitlets.traitlets.TraitType):
13
13
 
14
14
  # for py36 we are stuck with a version that does not have this
15
15
  if not hasattr(traitlets.traitlets, "Callable"):
16
- traitlets.traitlets.Callable = Callable
17
- traitlets.Callable = Callable
16
+ traitlets.traitlets.Callable = Callable # type: ignore
17
+ traitlets.Callable = Callable # type: ignore
18
18
 
19
19
  if not hasattr(traitlets.traitlets.TraitType, "default"):
20
20
 
@@ -33,4 +33,4 @@ if not hasattr(traitlets.traitlets.TraitType, "default"):
33
33
  # Undefined will raise in TraitType.get
34
34
  return self.default_value
35
35
 
36
- traitlets.traitlets.TraitType.default = default
36
+ traitlets.traitlets.TraitType.default = default # type: ignore
@@ -0,0 +1,45 @@
1
+ from IPython.core.formatters import BaseFormatter
2
+ from IPython.core.interactiveshell import InteractiveShell
3
+
4
+ ipython_display_formatter_original = InteractiveShell.instance().display_formatter.ipython_display_formatter
5
+ original_display_publisher_publish = InteractiveShell.instance().display_pub.publish
6
+
7
+
8
+ def publish(data, metadata=None, *args, **kwargs):
9
+ """Will intercept a display call and add the display data to an output widget when in a reacton context/render function."""
10
+ from .core import get_render_context
11
+
12
+ rc = get_render_context(required=False)
13
+ if rc is not None:
14
+ from .ipywidgets import Output
15
+
16
+ Output(outputs=[{"output_type": "display_data", "data": data, "metadata": metadata}])
17
+ else:
18
+ return original_display_publisher_publish(data, metadata, *args, **kwargs)
19
+
20
+
21
+ class ReactonDisplayFormatter(BaseFormatter):
22
+ """Add direct support for adding elements to a container.
23
+
24
+ Example:
25
+
26
+ with w.VBox():
27
+ display(button)
28
+
29
+ """
30
+
31
+ def __call__(self, obj):
32
+ from .core import Element, get_render_context # noqa
33
+
34
+ rc = get_render_context(required=False)
35
+ if rc is not None:
36
+ if rc.container_adders:
37
+ if isinstance(obj, Element):
38
+ # add directly as a child
39
+ rc.container_adders[-1].add(obj)
40
+ return True # we handled it
41
+ return ipython_display_formatter_original(obj)
42
+
43
+
44
+ InteractiveShell.instance().display_formatter.ipython_display_formatter = ReactonDisplayFormatter()
45
+ InteractiveShell.instance().display_pub.publish = publish
@@ -45,7 +45,7 @@ def equals(a, b):
45
45
  return same_component(a.component, b.component) and equals(a.args, b.args) and equals(a.kwargs, b.kwargs)
46
46
  elif isinstance(a, types.FunctionType):
47
47
  return a.__code__ == b.__code__ and (
48
- (a.__closure__ == b.__closure__) or equals([c.cell_contents for c in a.__closure__ or ()], [c.cell_contents for c in b.__closure__ or ()])
48
+ (equals(a.__closure__, b.__closure__)) or equals([c.cell_contents for c in a.__closure__ or ()], [c.cell_contents for c in b.__closure__ or ()])
49
49
  )
50
50
  elif isinstance(a, dict) and isinstance(b, dict):
51
51
  if len(a) != len(b):
@@ -51,15 +51,15 @@ def test_equals():
51
51
  assert equals(dar1, dar2)
52
52
  assert not equals(dar1, dar3)
53
53
 
54
- def make_function(a):
54
+ def make_function(a, ar1, ar2):
55
55
  def func(x):
56
- return x**a
56
+ return x**a + (ar1 == ar2)
57
57
 
58
58
  return func
59
59
 
60
- f1 = make_function(1)
61
- f2 = make_function(1)
62
- f3 = make_function(2)
60
+ f1 = make_function(1, ar1, ar2)
61
+ f2 = make_function(1, ar1, ar2)
62
+ f3 = make_function(2, ar2, ar2)
63
63
  assert f1 == f1
64
64
  assert f1 != f2
65
65
  assert equals(f1, f1)
@@ -1 +0,0 @@
1
- __version__ = "1.1.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