pythonnative 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

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.
@@ -11,8 +11,12 @@ Supports:
11
11
  ``@component``). Their hook state is preserved across renders.
12
12
  - **Provider elements** (type ``"__Provider__"``), which push/pop
13
13
  context values during tree traversal.
14
+ - **Error boundary elements** (type ``"__ErrorBoundary__"``), which
15
+ catch exceptions in child subtrees and render a fallback.
14
16
  - **Key-based child reconciliation** for stable identity across
15
17
  re-renders.
18
+ - **Post-render effect flushing**: after each mount or reconcile pass,
19
+ all queued effects are executed so they see the committed native tree.
16
20
  """
17
21
 
18
22
  from typing import Any, List, Optional
@@ -36,6 +40,10 @@ class VNode:
36
40
  class Reconciler:
37
41
  """Create, diff, and patch native view trees from Element descriptors.
38
42
 
43
+ After each ``mount`` or ``reconcile`` call the reconciler walks the
44
+ committed tree and flushes all pending effects so that effect
45
+ callbacks run **after** native mutations are applied.
46
+
39
47
  Parameters
40
48
  ----------
41
49
  backend:
@@ -56,6 +64,7 @@ class Reconciler:
56
64
  def mount(self, element: Element) -> Any:
57
65
  """Build native views from *element* and return the root native view."""
58
66
  self._tree = self._create_tree(element)
67
+ self._flush_effects()
59
68
  return self._tree.native_view
60
69
 
61
70
  def reconcile(self, new_element: Element) -> Any:
@@ -65,11 +74,28 @@ class Reconciler:
65
74
  """
66
75
  if self._tree is None:
67
76
  self._tree = self._create_tree(new_element)
77
+ self._flush_effects()
68
78
  return self._tree.native_view
69
79
 
70
80
  self._tree = self._reconcile_node(self._tree, new_element)
81
+ self._flush_effects()
71
82
  return self._tree.native_view
72
83
 
84
+ # ------------------------------------------------------------------
85
+ # Effect flushing
86
+ # ------------------------------------------------------------------
87
+
88
+ def _flush_effects(self) -> None:
89
+ """Walk the committed tree and flush pending effects (depth-first)."""
90
+ if self._tree is not None:
91
+ self._flush_tree_effects(self._tree)
92
+
93
+ def _flush_tree_effects(self, node: VNode) -> None:
94
+ for child in node.children:
95
+ self._flush_tree_effects(child)
96
+ if node.hook_state is not None:
97
+ node.hook_state.flush_pending_effects()
98
+
73
99
  # ------------------------------------------------------------------
74
100
  # Internal helpers
75
101
  # ------------------------------------------------------------------
@@ -87,6 +113,10 @@ class Reconciler:
87
113
  children = [child_node] if child_node else []
88
114
  return VNode(element, native_view, children)
89
115
 
116
+ # Error boundary: catch exceptions in the child subtree
117
+ if element.type == "__ErrorBoundary__":
118
+ return self._create_error_boundary(element)
119
+
90
120
  # Function component: call with hook context
91
121
  if callable(element.type):
92
122
  from .hooks import HookState, _set_hook_state
@@ -114,6 +144,20 @@ class Reconciler:
114
144
  children.append(child_node)
115
145
  return VNode(element, native_view, children)
116
146
 
147
+ def _create_error_boundary(self, element: Element) -> VNode:
148
+ fallback_fn = element.props.get("__fallback__")
149
+ try:
150
+ child_node = self._create_tree(element.children[0]) if element.children else None
151
+ except Exception as exc:
152
+ if fallback_fn is not None:
153
+ fallback_el = fallback_fn(exc) if callable(fallback_fn) else fallback_fn
154
+ child_node = self._create_tree(fallback_el)
155
+ else:
156
+ raise
157
+ native_view = child_node.native_view if child_node else None
158
+ children = [child_node] if child_node else []
159
+ return VNode(element, native_view, children)
160
+
117
161
  def _reconcile_node(self, old: VNode, new_el: Element) -> VNode:
118
162
  if not self._same_type(old.element, new_el):
119
163
  new_node = self._create_tree(new_el)
@@ -138,6 +182,10 @@ class Reconciler:
138
182
  old.element = new_el
139
183
  return old
140
184
 
185
+ # Error boundary
186
+ if new_el.type == "__ErrorBoundary__":
187
+ return self._reconcile_error_boundary(old, new_el)
188
+
141
189
  # Function component
142
190
  if callable(new_el.type):
143
191
  from .hooks import _set_hook_state
@@ -175,10 +223,34 @@ class Reconciler:
175
223
  old.element = new_el
176
224
  return old
177
225
 
226
+ def _reconcile_error_boundary(self, old: VNode, new_el: Element) -> VNode:
227
+ fallback_fn = new_el.props.get("__fallback__")
228
+ try:
229
+ if old.children and new_el.children:
230
+ child = self._reconcile_node(old.children[0], new_el.children[0])
231
+ old.children = [child]
232
+ old.native_view = child.native_view
233
+ elif new_el.children:
234
+ child = self._create_tree(new_el.children[0])
235
+ old.children = [child]
236
+ old.native_view = child.native_view
237
+ except Exception as exc:
238
+ for c in old.children:
239
+ self._destroy_tree(c)
240
+ if fallback_fn is not None:
241
+ fallback_el = fallback_fn(exc) if callable(fallback_fn) else fallback_fn
242
+ child = self._create_tree(fallback_el)
243
+ old.children = [child]
244
+ old.native_view = child.native_view
245
+ else:
246
+ raise
247
+ old.element = new_el
248
+ return old
249
+
178
250
  def _reconcile_children(self, parent: VNode, new_children: List[Element]) -> None:
179
251
  old_children = parent.children
180
252
  parent_type = parent.element.type
181
- is_native = isinstance(parent_type, str) and parent_type != "__Provider__"
253
+ is_native = isinstance(parent_type, str) and parent_type not in ("__Provider__", "__ErrorBoundary__")
182
254
 
183
255
  old_by_key: dict = {}
184
256
  old_unkeyed: list = []
@@ -215,7 +287,11 @@ class Reconciler:
215
287
  self.backend.insert_child(parent.native_view, node.native_view, parent_type, i)
216
288
  new_child_nodes.append(node)
217
289
  else:
290
+ old_native = matched.native_view
218
291
  updated = self._reconcile_node(matched, new_el)
292
+ if is_native and updated.native_view is not old_native:
293
+ self.backend.remove_child(parent.native_view, old_native, parent_type)
294
+ self.backend.insert_child(parent.native_view, updated.native_view, parent_type, i)
219
295
  new_child_nodes.append(updated)
220
296
 
221
297
  # Destroy unused old nodes
@@ -229,6 +305,18 @@ class Reconciler:
229
305
  self.backend.remove_child(parent.native_view, node.native_view, parent_type)
230
306
  self._destroy_tree(node)
231
307
 
308
+ # Reorder native children when keyed children changed positions.
309
+ # Without this, native sibling order drifts from the logical tree
310
+ # when keyed children swap positions across reconcile passes.
311
+ if is_native and used_keyed:
312
+ old_key_order = [c.element.key for c in old_children if c.element.key in used_keyed]
313
+ new_key_order = [n.element.key for n in new_child_nodes if n.element.key in used_keyed]
314
+ if old_key_order != new_key_order:
315
+ for node in new_child_nodes:
316
+ self.backend.remove_child(parent.native_view, node.native_view, parent_type)
317
+ for node in new_child_nodes:
318
+ self.backend.add_child(parent.native_view, node.native_view, parent_type)
319
+
232
320
  parent.children = new_child_nodes
233
321
 
234
322
  def _destroy_tree(self, node: VNode) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -1,11 +1,11 @@
1
- pythonnative/__init__.py,sha256=jhPsaI_qyTEQ_YMT5pJumYOQKYamfHbPalWd5VbJ5vY,1575
2
- pythonnative/components.py,sha256=dvLaXmCFU8O6bnYdudwBVsvad1DkwvmMJoTZRAZi540,10534
1
+ pythonnative/__init__.py,sha256=PJ5VzzASR3xUdgP1iPlGtwXaxzNmC-c_UlJeKu0mCBQ,2038
2
+ pythonnative/components.py,sha256=zF24vXM6halq-fweLtLHxZrmBrUKCa9c44hf9Bych-Y,12767
3
3
  pythonnative/element.py,sha256=RBUsXzzzM7KdK-NqMD-InVPKdAb8XJ0h0VpI2rwsfHs,1795
4
- pythonnative/hooks.py,sha256=UMoNTFoMKON4uJpUU9WHBv60tR2GUvx4Q_OgfG24Xcg,10361
5
- pythonnative/hot_reload.py,sha256=GoooBkRm2_zu_bQGOsOvUx-x9GdFcvRwSWEkhltFg2Y,4498
6
- pythonnative/native_views.py,sha256=VuqseL-QRHsEoGe3j3Ubh-iPo0dAkx-rcieZqWlt5ho,62037
7
- pythonnative/page.py,sha256=Pjf2o-0ufmn_6q2hyRZ3zrnHWkDTm2PD6rI4niQTfS8,14803
8
- pythonnative/reconciler.py,sha256=4Ah8NtYawZH9ZHXoMMjMTfdrTkZ0b4OT3e_85WLTrbU,9850
4
+ pythonnative/hooks.py,sha256=bqnJvEpbYSlbwkBKphT5lLEXQ1mO6AlO4r9SCTBlu7w,13768
5
+ pythonnative/hot_reload.py,sha256=dtppJaQI6Rl7muCTMgLjFNsD4_F4yYnkEpliCLaaWm8,4508
6
+ pythonnative/navigation.py,sha256=dDY9qBPLJKr2NnvdIDs8Xy-6kFesQNCEaLjUDT-0_l8,19042
7
+ pythonnative/page.py,sha256=ya-UUk_0_lO2YLx3UmBjtPL0j1YXItzP3VhPIloRhXQ,16168
8
+ pythonnative/reconciler.py,sha256=EuB8InxqlbZpZ0qI8hSjwNQsS-lsvh0vdqGzTnmY1M8,14133
9
9
  pythonnative/style.py,sha256=NG58FSJCBTBBWRrDOyUiHZsTxQDA3jg2PSOcCsU2F5g,3882
10
10
  pythonnative/utils.py,sha256=IqR_GYknveM_NfAblcaizg9S66hCZfrfiH08HzpOc-4,2537
11
11
  pythonnative/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,6 +15,10 @@ pythonnative/native_modules/camera.py,sha256=b3UErkABhBm2nQ-e72lEeFOgXoVDY6245cV
15
15
  pythonnative/native_modules/file_system.py,sha256=2fvYsIboCtjEyxmVeV72glOjtc9fJN3jTM6uaccru6E,4478
16
16
  pythonnative/native_modules/location.py,sha256=bMSNbtG60hAorKXh4RR2vybX88M_BwHkCpTprRbMsZU,1883
17
17
  pythonnative/native_modules/notifications.py,sha256=g-zT1GD-ojPsLN5eGXWkyNHTncSxdiEYtjllCXlsBSc,5377
18
+ pythonnative/native_views/__init__.py,sha256=0weXhCaDT79sez9Q1JibXSgouRVB1tjucFibhbPQeYc,2984
19
+ pythonnative/native_views/android.py,sha256=59_VzpGlADHFrY2cG8llVn4sDmPkUSGyUVKbyvfkKiU,32686
20
+ pythonnative/native_views/base.py,sha256=IS4_AQd4WsHgn8fka8np9zpaKdAOn1q0Zo09a8wYOtE,4229
21
+ pythonnative/native_views/ios.py,sha256=G5bQz57v5WSw5-wNmpcrcdX3rb7isb126la7UlveZLA,30087
18
22
  pythonnative/templates/android_template/build.gradle,sha256=4gE6CRS6RuBu9kp-_e_uYYU9mBgHVZrqQg9caSxgyuc,352
19
23
  pythonnative/templates/android_template/gradle.properties,sha256=REPaKLRfQiiVfIV8wYmgwzPWvF1f3bhh_kAMV9p4HME,1358
20
24
  pythonnative/templates/android_template/gradlew,sha256=YxNShxF6Hm0SyEWA8fScYdG6AiGOzShmBgXpf5dufWU,5766
@@ -66,9 +70,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
66
70
  pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
67
71
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=l2Pwa50F_rv-qPu2go6e4bQernM6PTQJeNPFl_c4ivY,1387
68
72
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
69
- pythonnative-0.7.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
70
- pythonnative-0.7.0.dist-info/METADATA,sha256=aa5sabkb9mMAcl_PSvvkIeFfHKlkpOnxJZRkMLTc3gE,6692
71
- pythonnative-0.7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
72
- pythonnative-0.7.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
73
- pythonnative-0.7.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
74
- pythonnative-0.7.0.dist-info/RECORD,,
73
+ pythonnative-0.8.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
74
+ pythonnative-0.8.0.dist-info/METADATA,sha256=FZB-auwBCmhEBCt3nKpY7ZSNN3mF6_kbDLjVNWwfBMw,6692
75
+ pythonnative-0.8.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
76
+ pythonnative-0.8.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
77
+ pythonnative-0.8.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
78
+ pythonnative-0.8.0.dist-info/RECORD,,