pythonnative 0.23.0__py3-none-any.whl → 0.25.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.
pythonnative/screen.py CHANGED
@@ -27,7 +27,7 @@ Example:
27
27
  count, set_count = pn.use_state(0)
28
28
  return pn.Column(
29
29
  pn.Text(f"Count: {count}", style={"font_size": 24}),
30
- pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
30
+ pn.Button("Tap me", on_press=lambda: set_count(count + 1)),
31
31
  style={"spacing": 12, "padding": 16},
32
32
  )
33
33
  ```
@@ -48,8 +48,10 @@ import json
48
48
  import os
49
49
  import sys
50
50
  import threading
51
- from typing import Any, Dict, Optional, Sequence
51
+ import traceback
52
+ from typing import Any, Dict, Optional, Sequence, Tuple
52
53
 
54
+ from . import diagnostics
53
55
  from .utils import IS_ANDROID, IS_DESKTOP, IS_IOS, set_android_context
54
56
 
55
57
  _MAX_RENDER_PASSES = 25
@@ -186,6 +188,10 @@ def _init_host_common(host: Any, component_path: str, component_func: Any) -> No
186
188
  host._hot_reload_manifest_path = None
187
189
  host._hot_reload_last_version = None
188
190
  host._layout_listener = None # retained on Android to prevent GC
191
+ # RedBox state: a dedicated reconciler that mounts the dev error
192
+ # overlay in place of the app tree (see ``_show_redbox``).
193
+ host._redbox_reconciler = None
194
+ host._redbox_root = None
189
195
  # Focus state: drives ``use_focus_effect``. Starts focused because
190
196
  # a host is only created when the screen is being presented; the
191
197
  # platform lifecycle hooks (``on_resume`` / ``on_pause``) flip this
@@ -207,6 +213,54 @@ def _set_host_focused(host: Any, focused: bool) -> None:
207
213
  pass
208
214
 
209
215
 
216
+ def _destroy_host(host: Any) -> None:
217
+ """Tear down a screen host when the native screen is destroyed for good.
218
+
219
+ Called from each platform's ``on_destroy``. Unmounting the
220
+ reconciler runs every pending effect cleanup, destroys the native
221
+ views (releasing their event registrations), and clears back
222
+ handlers, so a popped screen doesn't leak its whole tree. Also
223
+ unregisters the host's RedBox reporter so runtime errors from other
224
+ screens don't route to a dead overlay.
225
+ """
226
+ _clear_redbox(host, reattach=False)
227
+ diagnostics.set_error_reporter(host, None)
228
+ reconciler = host._reconciler
229
+ host._reconciler = None
230
+ if reconciler is not None:
231
+ try:
232
+ reconciler.unmount()
233
+ except Exception:
234
+ _log_pn("_destroy_host: reconciler.unmount() failed")
235
+ root = host._root_native_view
236
+ host._root_native_view = None
237
+ if root is not None:
238
+ try:
239
+ host._detach_root(root)
240
+ except Exception:
241
+ pass
242
+ host._nav_handle = None
243
+ host._focus_subscribers = []
244
+
245
+
246
+ def _host_back_pressed(host: Any) -> bool:
247
+ """Offer the system back action to ``use_back_handler`` subscribers.
248
+
249
+ Called by the native templates before running the platform's
250
+ default back behavior. Returns ``True`` when a handler consumed
251
+ the event, in which case the platform must *not* pop the screen.
252
+ """
253
+ reconciler = host._reconciler
254
+ if reconciler is None:
255
+ return False
256
+ try:
257
+ return bool(reconciler.dispatch_back_press())
258
+ except Exception as exc:
259
+ if not diagnostics.report_error(exc, phase="back handler"):
260
+ traceback.print_exc()
261
+ return False
262
+
263
+
210
264
  def _push_viewport_size(host: Any, width: float, height: float) -> None:
211
265
  """Forward a viewport-size change to the reconciler.
212
266
 
@@ -223,6 +277,9 @@ def _push_viewport_size(host: Any, width: float, height: float) -> None:
223
277
  if width <= 0 or height <= 0:
224
278
  return
225
279
  host._reconciler.set_viewport_size(float(width), float(height))
280
+ redbox = getattr(host, "_redbox_reconciler", None)
281
+ if redbox is not None:
282
+ redbox.set_viewport_size(float(width), float(height))
226
283
  try:
227
284
  from . import platform_metrics
228
285
 
@@ -269,6 +326,32 @@ def _flush_scheduled_renders(hosts: Sequence[Any]) -> None:
269
326
  _re_render(host)
270
327
 
271
328
 
329
+ def _seed_initial_viewport(host: Any) -> None:
330
+ """Give the reconciler a plausible viewport size *before* the first mount.
331
+
332
+ The authoritative size arrives right after attach (Android's
333
+ ``OnLayoutChangeListener``, iOS ``viewDidLayoutSubviews``, the
334
+ desktop stage size), but by then the mount commit has already run:
335
+ without a viewport its layout pass is skipped, so mount-time
336
+ [`use_layout_effect`][pythonnative.use_layout_effect] callbacks
337
+ would observe no committed frames. Hosts opt in by providing
338
+ ``_initial_viewport_size()``; the estimate only needs to be
339
+ plausible (screen-sized), not pixel-perfect.
340
+ """
341
+ getter = getattr(host, "_initial_viewport_size", None)
342
+ if not callable(getter):
343
+ return
344
+ try:
345
+ size = getter()
346
+ except Exception:
347
+ return
348
+ if not size:
349
+ return
350
+ width, height = size
351
+ if width > 0 and height > 0:
352
+ host._reconciler.set_viewport_size(float(width), float(height))
353
+
354
+
272
355
  def _on_create(host: Any) -> None:
273
356
  from .hooks import NavigationHandle, Provider, _NavigationContext
274
357
 
@@ -285,23 +368,30 @@ def _on_create(host: Any) -> None:
285
368
  # If we're already mounted, just re-attach the existing root view
286
369
  # to the (newly created) native container; ``on_resume`` will
287
370
  # fire the focus subscribers separately.
371
+ _register_redbox_reporter(host)
288
372
  if host._reconciler is not None and host._root_native_view is not None:
289
373
  host._attach_root(host._root_native_view)
290
374
  return
291
375
 
292
376
  host._nav_handle = NavigationHandle(host)
293
377
  host._reconciler = _new_reconciler(host)
378
+ _seed_initial_viewport(host)
294
379
 
295
- app_element = _render_app(host)
296
- provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
297
-
298
- host._is_rendering = True
299
380
  try:
300
- host._root_native_view = host._reconciler.mount(provider_element)
301
- host._attach_root(host._root_native_view)
302
- _drain_renders(host)
303
- finally:
304
- host._is_rendering = False
381
+ app_element = _render_app(host)
382
+ provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
383
+
384
+ host._is_rendering = True
385
+ try:
386
+ host._root_native_view = host._reconciler.mount(provider_element)
387
+ host._attach_root(host._root_native_view)
388
+ _drain_renders(host)
389
+ finally:
390
+ host._is_rendering = False
391
+ except Exception as exc:
392
+ if not diagnostics.is_dev():
393
+ raise
394
+ _show_redbox(host, exc, phase="mount")
305
395
 
306
396
 
307
397
  def _request_render(host: Any) -> None:
@@ -333,13 +423,18 @@ def _re_render(host: Any) -> None:
333
423
  hot reload.
334
424
  """
335
425
  _log_pn("_re_render: starting local render pass")
336
- host._is_rendering = True
337
426
  try:
338
- host._render_queued = False
339
- _commit_dirty(host)
340
- _drain_renders(host)
341
- finally:
342
- host._is_rendering = False
427
+ host._is_rendering = True
428
+ try:
429
+ host._render_queued = False
430
+ _commit_dirty(host)
431
+ _drain_renders(host)
432
+ finally:
433
+ host._is_rendering = False
434
+ except Exception as exc:
435
+ if not diagnostics.is_dev():
436
+ raise
437
+ _show_redbox(host, exc, phase="render")
343
438
  _log_pn("_re_render: done")
344
439
 
345
440
 
@@ -367,6 +462,130 @@ def _drain_renders(host: Any) -> None:
367
462
  _commit_dirty(host)
368
463
 
369
464
 
465
+ # ======================================================================
466
+ # RedBox (dev-mode error overlay)
467
+ # ======================================================================
468
+ #
469
+ # In dev mode (``pn preview``, ``pn run`` with hot reload, ``PN_DEV=1``)
470
+ # an uncaught error from a mount, render, effect, or event handler
471
+ # replaces the screen with a full-screen traceback instead of crashing
472
+ # the process or dying silently in the log. The overlay is a normal
473
+ # PythonNative element tree mounted by a *dedicated* reconciler so a
474
+ # broken app tree can't take the error display down with it. Fixing the
475
+ # code and saving clears it via hot reload; the Dismiss button restores
476
+ # the previous native root (which is still mounted underneath).
477
+
478
+
479
+ def _redbox_element(exc: BaseException, phase: str, on_dismiss: Any) -> Any:
480
+ """Build the RedBox element tree for ``exc``."""
481
+ from .components import Button, Column, ScrollView, Text
482
+
483
+ trace = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
484
+ message = str(exc) or "(no message)"
485
+ return Column(
486
+ Column(
487
+ Text(
488
+ f"{type(exc).__name__} in {phase}",
489
+ style={"color": "#FFD3DA", "font_size": 13, "bold": True},
490
+ ),
491
+ Text(message, style={"color": "#FFFFFF", "font_size": 17, "bold": True}),
492
+ style={
493
+ "background_color": "#C4283C",
494
+ "padding": 16,
495
+ "padding_top": 56,
496
+ "spacing": 6,
497
+ },
498
+ ),
499
+ ScrollView(
500
+ Text(trace, style={"color": "#FF9AA8", "font_size": 12}),
501
+ style={"flex": 1, "padding": 12},
502
+ ),
503
+ Column(
504
+ Button("Dismiss", on_press=on_dismiss, style={"color": "#FFFFFF"}),
505
+ Text(
506
+ "Fix the error and save to reload.",
507
+ style={"color": "#8E8E93", "font_size": 12, "text_align": "center"},
508
+ ),
509
+ style={"padding": 12, "padding_bottom": 32, "spacing": 4},
510
+ ),
511
+ style={"flex": 1, "background_color": "#1C1C1E"},
512
+ )
513
+
514
+
515
+ def _show_redbox(host: Any, exc: BaseException, phase: str = "render") -> None:
516
+ """Mount the dev error overlay over the host's screen.
517
+
518
+ Safe to call repeatedly (a newer error replaces the current
519
+ overlay) and from any thread (the mount hops to the main thread).
520
+ Failures inside the RedBox itself fall back to printing both
521
+ tracebacks so the original error is never lost.
522
+ """
523
+ _log_pn(f"_show_redbox: {type(exc).__name__} during {phase}")
524
+ try:
525
+ print(f"[PN] {phase} error:", file=sys.stderr)
526
+ traceback.print_exception(type(exc), exc, exc.__traceback__)
527
+ except Exception:
528
+ pass
529
+
530
+ def _mount() -> None:
531
+ try:
532
+ _clear_redbox(host, reattach=False)
533
+ from .native_views import get_registry
534
+ from .reconciler import Reconciler
535
+
536
+ redbox = Reconciler(get_registry())
537
+ redbox._screen_re_render = redbox.flush_dirty
538
+ element = _redbox_element(exc, phase, lambda: _clear_redbox(host))
539
+ root = redbox.mount(element)
540
+ width, height = (0.0, 0.0)
541
+ if host._reconciler is not None:
542
+ width, height = host._reconciler._viewport_size
543
+ if width <= 0 or height <= 0:
544
+ from . import platform_metrics
545
+
546
+ dims = platform_metrics.get_window_dimensions()
547
+ width, height = dims.width, dims.height
548
+ if width > 0 and height > 0:
549
+ redbox.set_viewport_size(width, height)
550
+ host._redbox_reconciler = redbox
551
+ host._redbox_root = root
552
+ if host._root_native_view is not None:
553
+ host._detach_root(host._root_native_view)
554
+ host._attach_root(root)
555
+ except Exception:
556
+ print("[PN] RedBox failed to mount:", file=sys.stderr)
557
+ traceback.print_exc()
558
+
559
+ from .runtime import call_on_main_thread
560
+
561
+ call_on_main_thread(_mount)
562
+
563
+
564
+ def _clear_redbox(host: Any, reattach: bool = True) -> None:
565
+ """Tear down the RedBox overlay and optionally restore the app root."""
566
+ redbox = getattr(host, "_redbox_reconciler", None)
567
+ if redbox is None:
568
+ return
569
+ host._redbox_reconciler = None
570
+ host._redbox_root = None
571
+ try:
572
+ redbox.unmount()
573
+ except Exception:
574
+ pass
575
+ if reattach and host._root_native_view is not None:
576
+ try:
577
+ host._attach_root(host._root_native_view)
578
+ except Exception:
579
+ pass
580
+
581
+
582
+ def _register_redbox_reporter(host: Any) -> None:
583
+ """Route runtime errors (events, async) for this host to the RedBox."""
584
+ if not diagnostics.is_dev():
585
+ return
586
+ diagnostics.set_error_reporter(host, lambda exc, phase: _show_redbox(host, exc, phase))
587
+
588
+
370
589
  def _set_args(host: Any, args: Any) -> None:
371
590
  if isinstance(args, str):
372
591
  try:
@@ -380,6 +599,10 @@ def _set_args(host: Any, args: Any) -> None:
380
599
  def _enable_hot_reload(host: Any, manifest_path: str) -> None:
381
600
  host._hot_reload_manifest_path = manifest_path
382
601
  host._hot_reload_last_version = None
602
+ # Hot reload only runs on debug builds, so treat it as the on-device
603
+ # dev-mode switch (validation warnings, hook-order checks, RedBox).
604
+ diagnostics.set_dev_mode(True)
605
+ _register_redbox_reporter(host)
383
606
 
384
607
 
385
608
  def _hot_reload_tick(host: Any) -> bool:
@@ -473,8 +696,13 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
473
696
 
474
697
  try:
475
698
  new_component = _import_component(host._component_path)
476
- except Exception as e:
477
- _log_pn(f"_reload_host: re-import failed: {e!r}; aborting reload")
699
+ except Exception as exc:
700
+ _log_pn(f"_reload_host: re-import failed: {exc!r}; aborting reload")
701
+ if diagnostics.is_dev():
702
+ # A syntax error (or import-time crash) in the edited file:
703
+ # show it in the RedBox so the developer sees it on device
704
+ # instead of the app silently keeping the old code.
705
+ _show_redbox(host, exc, phase="hot reload import")
478
706
  return
479
707
  host._component = new_component
480
708
 
@@ -482,11 +710,22 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
482
710
  _log_pn(f"_reload_host: host=0x{id(host):x} reconciler=None; skipping refresh")
483
711
  return
484
712
 
713
+ # A reload always replaces whatever error state was on screen.
714
+ # Re-attach the current root now: Fast Refresh patches that tree in
715
+ # place (attach must happen regardless of whether it succeeds), and
716
+ # a full remount swaps in its own fresh root anyway.
717
+ _clear_redbox(host)
718
+
485
719
  if _try_fast_refresh(host, reloaded):
486
720
  print(f"[hot-reload] Fast Refresh: {', '.join(requested) or ', '.join(reloaded)}", file=sys.stderr)
487
721
  return
488
722
 
489
- _full_remount(host, reloaded)
723
+ try:
724
+ _full_remount(host, reloaded)
725
+ except Exception as exc:
726
+ if not diagnostics.is_dev():
727
+ raise
728
+ _show_redbox(host, exc, phase="hot reload")
490
729
 
491
730
 
492
731
  def _try_fast_refresh(host: Any, reloaded_modules: Sequence[str]) -> bool:
@@ -819,6 +1058,15 @@ if IS_ANDROID:
819
1058
  set_android_context(native_instance)
820
1059
  _init_host_common(self, component_path, component_func)
821
1060
 
1061
+ def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
1062
+ """Estimate the viewport from display metrics (pre-attach)."""
1063
+ try:
1064
+ metrics = self.native_instance.getResources().getDisplayMetrics()
1065
+ density = float(metrics.density) or 1.0
1066
+ return (metrics.widthPixels / density, metrics.heightPixels / density)
1067
+ except Exception:
1068
+ return None
1069
+
822
1070
  def on_create(self) -> None:
823
1071
  _android_publish_color_scheme(self.native_instance)
824
1072
  _on_create(self)
@@ -844,7 +1092,10 @@ if IS_ANDROID:
844
1092
  pass
845
1093
 
846
1094
  def on_destroy(self) -> None:
847
- pass
1095
+ _destroy_host(self)
1096
+
1097
+ def on_back_pressed(self) -> bool:
1098
+ return _host_back_pressed(self)
848
1099
 
849
1100
  def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
850
1101
  _enable_hot_reload(self, manifest_path)
@@ -938,11 +1189,18 @@ if IS_ANDROID:
938
1189
  _android_push_initial_viewport(self, container)
939
1190
 
940
1191
  def _detach_root(self, native_view: Any) -> None:
1192
+ # Remove this host's specific root view from whatever parent
1193
+ # holds it. Never clear the shared fragment container: when a
1194
+ # fragment is popped, its ``onDestroy`` (and therefore this
1195
+ # detach) runs *after* the screen below has already re-attached
1196
+ # its own root to the container, so a ``removeAllViews()`` here
1197
+ # would blank the restored screen.
1198
+ if native_view is None:
1199
+ return
941
1200
  try:
942
- from .utils import get_android_fragment_container
943
-
944
- container = get_android_fragment_container()
945
- container.removeAllViews()
1201
+ parent = native_view.getParent()
1202
+ if parent is not None:
1203
+ parent.removeView(native_view)
946
1204
  except Exception:
947
1205
  pass
948
1206
 
@@ -1005,6 +1263,17 @@ elif IS_DESKTOP:
1005
1263
  self.native_instance = native_instance
1006
1264
  _init_host_common(self, component_path, component_func)
1007
1265
 
1266
+ def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
1267
+ """Read the stage size from the DesktopApp controller (pre-attach)."""
1268
+ app = self.native_instance
1269
+ if app is None or not hasattr(app, "viewport_size"):
1270
+ return None
1271
+ try:
1272
+ width, height = app.viewport_size()
1273
+ return (float(width), float(height))
1274
+ except Exception:
1275
+ return None
1276
+
1008
1277
  def on_create(self) -> None:
1009
1278
  _on_create(self)
1010
1279
 
@@ -1024,7 +1293,10 @@ elif IS_DESKTOP:
1024
1293
  pass
1025
1294
 
1026
1295
  def on_destroy(self) -> None:
1027
- pass
1296
+ _destroy_host(self)
1297
+
1298
+ def on_back_pressed(self) -> bool:
1299
+ return _host_back_pressed(self)
1028
1300
 
1029
1301
  def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
1030
1302
  _enable_hot_reload(self, manifest_path)
@@ -1315,6 +1587,19 @@ else:
1315
1587
  return True
1316
1588
  _log_pn("_request_render: native iOS scheduler unavailable; render remains queued")
1317
1589
  return True
1590
+ # Defer via the main dispatch queue rather than an NSTimer.
1591
+ # GCD main-queue blocks are serviced in the common run-loop
1592
+ # modes, so they still run while UIKit is in tracking mode
1593
+ # (scrolls, synthesized test touches). A default-mode NSTimer
1594
+ # is starved for the whole gesture, which delayed renders
1595
+ # long enough on slow CI simulators for Maestro asserts to
1596
+ # time out, and let stale flushes land mid-touch.
1597
+ from .runtime import _ensure_libdispatch_loaded, _ios_dispatch_async
1598
+
1599
+ if _ensure_libdispatch_loaded():
1600
+ _ios_dispatch_async(_flush_ios_scheduled_renders)
1601
+ _log_pn("_request_render: deferred iOS render to next main-queue turn")
1602
+ return True
1318
1603
  try:
1319
1604
  NSTimer = ObjCClass("NSTimer")
1320
1605
  target = _ensure_ios_render_scheduler_target()
@@ -1352,6 +1637,24 @@ else:
1352
1637
  if self.native_instance is not None:
1353
1638
  _ios_register_screen(self.native_instance, self)
1354
1639
 
1640
+ def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
1641
+ """Estimate the viewport from the VC's view bounds (pre-attach)."""
1642
+ try:
1643
+ if self.native_instance is not None:
1644
+ bounds = self.native_instance.view.bounds
1645
+ width = float(bounds.size.width)
1646
+ height = float(bounds.size.height)
1647
+ if width > 0 and height > 0:
1648
+ return (width, height)
1649
+ except Exception:
1650
+ pass
1651
+ try:
1652
+ UIScreen = ObjCClass("UIScreen")
1653
+ screen_bounds = UIScreen.mainScreen.bounds
1654
+ return (float(screen_bounds.size.width), float(screen_bounds.size.height))
1655
+ except Exception:
1656
+ return None
1657
+
1355
1658
  def on_create(self) -> None:
1356
1659
  _ios_publish_color_scheme()
1357
1660
  _on_create(self)
@@ -1368,6 +1671,10 @@ else:
1368
1671
  def on_destroy(self) -> None:
1369
1672
  if self.native_instance is not None:
1370
1673
  _ios_unregister_screen(self.native_instance)
1674
+ _destroy_host(self)
1675
+
1676
+ def on_back_pressed(self) -> bool:
1677
+ return _host_back_pressed(self)
1371
1678
 
1372
1679
  def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
1373
1680
  _enable_hot_reload(self, manifest_path)
@@ -1425,22 +1732,55 @@ else:
1425
1732
  raise RuntimeError(
1426
1733
  "No UINavigationController available; ensure template embeds root in navigation controller"
1427
1734
  )
1428
- nav.pushViewController_animated_(next_vc, True)
1735
+ self._run_nav_op(lambda n: n.pushViewController_animated_(next_vc, True))
1429
1736
 
1430
1737
  def _pop(self) -> None:
1431
- nav = getattr(self.native_instance, "navigationController", None)
1432
- if nav is not None:
1433
- nav.popViewControllerAnimated_(True)
1738
+ self._run_nav_op(lambda n: n.popViewControllerAnimated_(True))
1434
1739
 
1435
1740
  def _reset_to_root(self) -> None:
1436
1741
  """Pop everything above the root view-controller."""
1437
- nav = getattr(self.native_instance, "navigationController", None)
1438
- if nav is not None:
1742
+
1743
+ def op(n: Any) -> None:
1439
1744
  try:
1440
- nav.popToRootViewControllerAnimated_(True)
1745
+ n.popToRootViewControllerAnimated_(True)
1441
1746
  except Exception:
1442
1747
  pass
1443
1748
 
1749
+ self._run_nav_op(op)
1750
+
1751
+ def _run_nav_op(self, op: Any) -> None:
1752
+ """Run ``op(nav)`` unless a stack transition is mid-flight.
1753
+
1754
+ UIKit does not queue navigation calls: a
1755
+ ``pushViewController`` / ``popViewController`` issued
1756
+ while the previous transition is still animating is
1757
+ silently ignored or, worse, misapplies and pops past
1758
+ the intended screen. A fast tap right after a push
1759
+ (easy for a user, common for a UI test on a slow
1760
+ machine) lands exactly in that window. Dropping the
1761
+ operation keeps the stack consistent: the tap simply
1762
+ does nothing, and a retry lands on a settled stack.
1763
+ (Deferring the operation instead was tried and is
1764
+ worse: the user retries, then the deferred operation
1765
+ also fires, popping one screen too many.)
1766
+ """
1767
+ nav = getattr(self.native_instance, "navigationController", None)
1768
+ if nav is None:
1769
+ return
1770
+ try:
1771
+ # rubicon resolves ``transitionCoordinator`` as a bound
1772
+ # method rather than a property on some classes; call
1773
+ # it to get the actual (possibly nil -> None) value.
1774
+ coord = nav.transitionCoordinator
1775
+ if callable(coord):
1776
+ coord = coord()
1777
+ if coord is not None:
1778
+ _log_pn("_run_nav_op: transition in flight; dropping nav op")
1779
+ return
1780
+ except Exception:
1781
+ pass
1782
+ op(nav)
1783
+
1444
1784
  def _set_screen_options(self, options: Dict[str, Any]) -> None:
1445
1785
  """Bind screen options (e.g. title) to the native nav bar.
1446
1786
 
@@ -1631,7 +1971,10 @@ else:
1631
1971
  pass
1632
1972
 
1633
1973
  def on_destroy(self) -> None:
1634
- pass
1974
+ _destroy_host(self)
1975
+
1976
+ def on_back_pressed(self) -> bool:
1977
+ return _host_back_pressed(self)
1635
1978
 
1636
1979
  def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
1637
1980
  _enable_hot_reload(self, manifest_path)
pythonnative/storage.py CHANGED
@@ -375,11 +375,11 @@ def use_persisted_state(
375
375
  theme, set_theme = pn.use_persisted_state("settings.theme", "light")
376
376
  return pn.Button(
377
377
  f"Theme: {theme}",
378
- on_click=lambda: set_theme("dark" if theme == "light" else "light"),
378
+ on_press=lambda: set_theme("dark" if theme == "light" else "light"),
379
379
  )
380
380
  ```
381
381
  """
382
- from .hooks import use_async_effect, use_callback, use_ref, use_state
382
+ from .hooks import use_callback, use_effect, use_ref, use_state
383
383
  from .runtime import run_async
384
384
 
385
385
  state, set_state = use_state(initial)
@@ -389,14 +389,14 @@ def use_persisted_state(
389
389
  stored = await AsyncStorage.get_json(key)
390
390
  if stored is not None:
391
391
  set_state(stored)
392
- loaded["current"] = True
392
+ loaded.current = True
393
393
 
394
- use_async_effect(_load, [key])
394
+ use_effect(_load, [key])
395
395
 
396
396
  def setter(value_or_updater: Any) -> None:
397
397
  def _reducer(current: Any) -> Any:
398
398
  new_value = value_or_updater(current) if callable(value_or_updater) else value_or_updater
399
- if loaded["current"] is True:
399
+ if loaded.current is True:
400
400
  run_async(AsyncStorage.set_json(key, new_value))
401
401
  return new_value
402
402