pythonnative 0.7.0__py3-none-any.whl → 0.9.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.
@@ -0,0 +1,571 @@
1
+ """Declarative navigation for PythonNative.
2
+
3
+ Provides a component-based navigation system inspired by React Navigation.
4
+ Navigators manage screen state in Python; they render the active screen's
5
+ component using the standard reconciler pipeline.
6
+
7
+ Usage::
8
+
9
+ from pythonnative.navigation import (
10
+ NavigationContainer,
11
+ create_stack_navigator,
12
+ create_tab_navigator,
13
+ create_drawer_navigator,
14
+ )
15
+
16
+ Stack = create_stack_navigator()
17
+
18
+ @pn.component
19
+ def App():
20
+ return NavigationContainer(
21
+ Stack.Navigator(
22
+ Stack.Screen("Home", component=HomeScreen),
23
+ Stack.Screen("Detail", component=DetailScreen),
24
+ )
25
+ )
26
+ """
27
+
28
+ from typing import Any, Callable, Dict, List, Optional
29
+
30
+ from .element import Element
31
+ from .hooks import (
32
+ Provider,
33
+ _NavigationContext,
34
+ component,
35
+ create_context,
36
+ use_context,
37
+ use_effect,
38
+ use_memo,
39
+ use_ref,
40
+ use_state,
41
+ )
42
+
43
+ # ======================================================================
44
+ # Focus context
45
+ # ======================================================================
46
+
47
+ _FocusContext = create_context(False)
48
+
49
+ # ======================================================================
50
+ # Data structures
51
+ # ======================================================================
52
+
53
+
54
+ class _ScreenDef:
55
+ """Configuration for a single screen within a navigator."""
56
+
57
+ __slots__ = ("name", "component", "options")
58
+
59
+ def __init__(self, name: str, component_fn: Any, options: Optional[Dict[str, Any]] = None) -> None:
60
+ self.name = name
61
+ self.component = component_fn
62
+ self.options = options or {}
63
+
64
+ def __repr__(self) -> str:
65
+ return f"Screen({self.name!r})"
66
+
67
+
68
+ class _RouteEntry:
69
+ """An entry in the navigation stack."""
70
+
71
+ __slots__ = ("name", "params")
72
+
73
+ def __init__(self, name: str, params: Optional[Dict[str, Any]] = None) -> None:
74
+ self.name = name
75
+ self.params = params or {}
76
+
77
+ def __repr__(self) -> str:
78
+ return f"Route({self.name!r})"
79
+
80
+
81
+ # ======================================================================
82
+ # Navigation handle for declarative navigators
83
+ # ======================================================================
84
+
85
+
86
+ class _DeclarativeNavHandle:
87
+ """Navigation handle provided by declarative navigators.
88
+
89
+ Implements the same interface as :class:`~pythonnative.hooks.NavigationHandle`
90
+ so that ``use_navigation()`` returns a compatible object regardless of
91
+ whether the app uses the legacy page-based navigation or declarative
92
+ navigators.
93
+
94
+ When *parent* is provided, unknown routes and root-level ``go_back``
95
+ calls are forwarded to the parent handle. This enables nested
96
+ navigators (e.g. a stack inside a tab) to delegate navigation actions
97
+ that they cannot handle locally.
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ screen_map: Dict[str, "_ScreenDef"],
103
+ get_stack: Callable[[], List["_RouteEntry"]],
104
+ set_stack: Callable,
105
+ parent: Any = None,
106
+ ) -> None:
107
+ self._screen_map = screen_map
108
+ self._get_stack = get_stack
109
+ self._set_stack = set_stack
110
+ self._parent = parent
111
+
112
+ def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
113
+ """Navigate to a named route, pushing it onto the stack.
114
+
115
+ If *route_name* is not known locally and a parent handle exists,
116
+ the call is forwarded to the parent navigator.
117
+ """
118
+ if route_name in self._screen_map:
119
+ entry = _RouteEntry(route_name, params)
120
+ self._set_stack(lambda s: list(s) + [entry])
121
+ elif self._parent is not None:
122
+ self._parent.navigate(route_name, params=params)
123
+ else:
124
+ raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
125
+
126
+ def go_back(self) -> None:
127
+ """Pop the current screen from the stack.
128
+
129
+ If the stack is at its root and a parent handle exists, the call
130
+ is forwarded to the parent navigator.
131
+ """
132
+ stack = self._get_stack()
133
+ if len(stack) > 1:
134
+ self._set_stack(lambda s: list(s[:-1]))
135
+ elif self._parent is not None:
136
+ self._parent.go_back()
137
+
138
+ def get_params(self) -> Dict[str, Any]:
139
+ """Return the parameters for the current route."""
140
+ stack = self._get_stack()
141
+ return stack[-1].params if stack else {}
142
+
143
+ def reset(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
144
+ """Reset the stack to a single route."""
145
+ if route_name not in self._screen_map:
146
+ raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
147
+ self._set_stack([_RouteEntry(route_name, params)])
148
+
149
+
150
+ class _TabNavHandle(_DeclarativeNavHandle):
151
+ """Navigation handle for tab navigators with tab switching."""
152
+
153
+ def __init__(
154
+ self,
155
+ screen_map: Dict[str, "_ScreenDef"],
156
+ get_stack: Callable[[], List["_RouteEntry"]],
157
+ set_stack: Callable,
158
+ switch_tab: Callable[[str, Optional[Dict[str, Any]]], None],
159
+ parent: Any = None,
160
+ ) -> None:
161
+ super().__init__(screen_map, get_stack, set_stack, parent=parent)
162
+ self._switch_tab = switch_tab
163
+
164
+ def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
165
+ """Switch to a tab by name, or forward to parent for unknown routes."""
166
+ if route_name in self._screen_map:
167
+ self._switch_tab(route_name, params)
168
+ elif self._parent is not None:
169
+ self._parent.navigate(route_name, params=params)
170
+ else:
171
+ raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
172
+
173
+
174
+ class _DrawerNavHandle(_DeclarativeNavHandle):
175
+ """Navigation handle for drawer navigators with open/close control."""
176
+
177
+ def __init__(
178
+ self,
179
+ screen_map: Dict[str, "_ScreenDef"],
180
+ get_stack: Callable[[], List["_RouteEntry"]],
181
+ set_stack: Callable,
182
+ switch_screen: Callable[[str, Optional[Dict[str, Any]]], None],
183
+ set_drawer_open: Callable[[bool], None],
184
+ get_drawer_open: Callable[[], bool],
185
+ parent: Any = None,
186
+ ) -> None:
187
+ super().__init__(screen_map, get_stack, set_stack, parent=parent)
188
+ self._switch_screen = switch_screen
189
+ self._set_drawer_open = set_drawer_open
190
+ self._get_drawer_open = get_drawer_open
191
+
192
+ def navigate(self, route_name: str, params: Optional[Dict[str, Any]] = None) -> None:
193
+ """Switch to a screen and close the drawer, or forward to parent."""
194
+ if route_name in self._screen_map:
195
+ self._switch_screen(route_name, params)
196
+ self._set_drawer_open(False)
197
+ elif self._parent is not None:
198
+ self._parent.navigate(route_name, params=params)
199
+ else:
200
+ raise ValueError(f"Unknown route: {route_name!r}. Known routes: {list(self._screen_map)}")
201
+
202
+ def open_drawer(self) -> None:
203
+ """Open the drawer."""
204
+ self._set_drawer_open(True)
205
+
206
+ def close_drawer(self) -> None:
207
+ """Close the drawer."""
208
+ self._set_drawer_open(False)
209
+
210
+ def toggle_drawer(self) -> None:
211
+ """Toggle the drawer open/closed."""
212
+ self._set_drawer_open(not self._get_drawer_open())
213
+
214
+
215
+ # ======================================================================
216
+ # Stack navigator
217
+ # ======================================================================
218
+
219
+
220
+ def _build_screen_map(screens: Any) -> Dict[str, "_ScreenDef"]:
221
+ """Build an ordered dict of name -> _ScreenDef from a list."""
222
+ result: Dict[str, _ScreenDef] = {}
223
+ for s in screens or []:
224
+ if isinstance(s, _ScreenDef):
225
+ result[s.name] = s
226
+ return result
227
+
228
+
229
+ @component
230
+ def _stack_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
231
+ screen_map = _build_screen_map(screens)
232
+ if not screen_map:
233
+ return Element("View", {}, [])
234
+
235
+ parent_nav = use_context(_NavigationContext)
236
+
237
+ first_route = initial_route or next(iter(screen_map))
238
+ stack, set_stack = use_state(lambda: [_RouteEntry(first_route)])
239
+
240
+ stack_ref = use_ref(None)
241
+ stack_ref["current"] = stack
242
+
243
+ handle = use_memo(
244
+ lambda: _DeclarativeNavHandle(screen_map, lambda: stack_ref["current"], set_stack, parent=parent_nav), []
245
+ )
246
+ handle._screen_map = screen_map
247
+ handle._parent = parent_nav
248
+
249
+ current = stack[-1]
250
+ screen_def = screen_map.get(current.name)
251
+ if screen_def is None:
252
+ return Element("Text", {"text": f"Unknown route: {current.name}"}, [])
253
+
254
+ screen_el = screen_def.component()
255
+ return Provider(_NavigationContext, handle, Provider(_FocusContext, True, screen_el))
256
+
257
+
258
+ def create_stack_navigator() -> Any:
259
+ """Create a stack-based navigator.
260
+
261
+ Returns an object with ``Navigator`` and ``Screen`` members::
262
+
263
+ Stack = create_stack_navigator()
264
+
265
+ Stack.Screen("Home", component=HomeScreen)
266
+
267
+ Stack.Navigator(
268
+ Stack.Screen("Home", component=HomeScreen),
269
+ Stack.Screen("Detail", component=DetailScreen),
270
+ initial_route="Home",
271
+ )
272
+ """
273
+
274
+ class _StackNavigator:
275
+ @staticmethod
276
+ def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
277
+ """Define a screen within this stack navigator."""
278
+ return _ScreenDef(name, component, options)
279
+
280
+ @staticmethod
281
+ def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
282
+ """Render the stack navigator with the given screens."""
283
+ return _stack_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
284
+
285
+ return _StackNavigator()
286
+
287
+
288
+ # ======================================================================
289
+ # Tab navigator
290
+ # ======================================================================
291
+
292
+
293
+ @component
294
+ def _tab_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
295
+ screen_list = list(screens or [])
296
+ screen_map = _build_screen_map(screen_list)
297
+ if not screen_map:
298
+ return Element("View", {}, [])
299
+
300
+ parent_nav = use_context(_NavigationContext)
301
+
302
+ first_route = initial_route or screen_list[0].name
303
+ active_tab, set_active_tab = use_state(first_route)
304
+ tab_params, set_tab_params = use_state(lambda: {first_route: {}})
305
+
306
+ params_ref = use_ref(None)
307
+ params_ref["current"] = tab_params
308
+
309
+ def switch_tab(name: str, params: Optional[Dict[str, Any]] = None) -> None:
310
+ set_active_tab(name)
311
+ if params:
312
+ set_tab_params(lambda p: {**p, name: params})
313
+
314
+ def get_stack() -> List[_RouteEntry]:
315
+ p = params_ref["current"] or {}
316
+ return [_RouteEntry(active_tab, p.get(active_tab, {}))]
317
+
318
+ handle = use_memo(lambda: _TabNavHandle(screen_map, get_stack, lambda _: None, switch_tab, parent=parent_nav), [])
319
+ handle._screen_map = screen_map
320
+ handle._switch_tab = switch_tab
321
+ handle._parent = parent_nav
322
+
323
+ screen_def = screen_map.get(active_tab)
324
+ if screen_def is None:
325
+ screen_def = screen_map[screen_list[0].name]
326
+
327
+ tab_items: List[Dict[str, str]] = []
328
+ for s in screen_list:
329
+ if isinstance(s, _ScreenDef):
330
+ tab_items.append({"name": s.name, "title": s.options.get("title", s.name)})
331
+
332
+ def on_tab_select(name: str) -> None:
333
+ switch_tab(name)
334
+
335
+ tab_bar = Element(
336
+ "TabBar",
337
+ {"items": tab_items, "active_tab": active_tab, "on_tab_select": on_tab_select},
338
+ [],
339
+ key="__tab_bar__",
340
+ )
341
+
342
+ screen_el = screen_def.component()
343
+ content = Provider(
344
+ _NavigationContext,
345
+ handle,
346
+ Provider(_FocusContext, True, screen_el),
347
+ )
348
+
349
+ return Element(
350
+ "View",
351
+ {"flex_direction": "column", "flex": 1},
352
+ [Element("View", {"flex": 1}, [content]), tab_bar],
353
+ )
354
+
355
+
356
+ def create_tab_navigator() -> Any:
357
+ """Create a tab-based navigator.
358
+
359
+ Returns an object with ``Navigator`` and ``Screen`` members::
360
+
361
+ Tab = create_tab_navigator()
362
+
363
+ Tab.Navigator(
364
+ Tab.Screen("Home", component=HomeScreen, options={"title": "Home"}),
365
+ Tab.Screen("Settings", component=SettingsScreen),
366
+ )
367
+ """
368
+
369
+ class _TabNavigator:
370
+ @staticmethod
371
+ def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
372
+ """Define a screen within this tab navigator."""
373
+ return _ScreenDef(name, component, options)
374
+
375
+ @staticmethod
376
+ def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
377
+ """Render the tab navigator with the given screens."""
378
+ return _tab_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
379
+
380
+ return _TabNavigator()
381
+
382
+
383
+ # ======================================================================
384
+ # Drawer navigator
385
+ # ======================================================================
386
+
387
+
388
+ @component
389
+ def _drawer_navigator_impl(screens: Any = None, initial_route: Optional[str] = None) -> Element:
390
+ screen_list = list(screens or [])
391
+ screen_map = _build_screen_map(screen_list)
392
+ if not screen_map:
393
+ return Element("View", {}, [])
394
+
395
+ parent_nav = use_context(_NavigationContext)
396
+
397
+ first_route = initial_route or screen_list[0].name
398
+ active_screen, set_active_screen = use_state(first_route)
399
+ drawer_open, set_drawer_open = use_state(False)
400
+ screen_params, set_screen_params = use_state(lambda: {first_route: {}})
401
+
402
+ params_ref = use_ref(None)
403
+ params_ref["current"] = screen_params
404
+
405
+ def switch_screen(name: str, params: Optional[Dict[str, Any]] = None) -> None:
406
+ set_active_screen(name)
407
+ if params:
408
+ set_screen_params(lambda p: {**p, name: params})
409
+
410
+ def get_stack() -> List[_RouteEntry]:
411
+ p = params_ref["current"] or {}
412
+ return [_RouteEntry(active_screen, p.get(active_screen, {}))]
413
+
414
+ handle = use_memo(
415
+ lambda: _DrawerNavHandle(
416
+ screen_map,
417
+ get_stack,
418
+ lambda _: None,
419
+ switch_screen,
420
+ set_drawer_open,
421
+ lambda: drawer_open,
422
+ parent=parent_nav,
423
+ ),
424
+ [],
425
+ )
426
+ handle._screen_map = screen_map
427
+ handle._switch_screen = switch_screen
428
+ handle._set_drawer_open = set_drawer_open
429
+ handle._get_drawer_open = lambda: drawer_open
430
+ handle._parent = parent_nav
431
+
432
+ screen_def = screen_map.get(active_screen)
433
+ if screen_def is None:
434
+ screen_def = screen_map[screen_list[0].name]
435
+
436
+ screen_el = screen_def.component()
437
+ content = Provider(
438
+ _NavigationContext,
439
+ handle,
440
+ Provider(_FocusContext, True, screen_el),
441
+ )
442
+
443
+ children: List[Element] = [Element("View", {"flex": 1}, [content])]
444
+
445
+ if drawer_open:
446
+ menu_items: List[Element] = []
447
+ for s in screen_list:
448
+ if not isinstance(s, _ScreenDef):
449
+ continue
450
+ label = s.options.get("title", s.name)
451
+ item_name = s.name
452
+
453
+ def make_select(n: str) -> Callable[[], None]:
454
+ def _select() -> None:
455
+ switch_screen(n)
456
+ set_drawer_open(False)
457
+
458
+ return _select
459
+
460
+ menu_items.append(
461
+ Element("Button", {"title": label, "on_click": make_select(item_name)}, [], key=f"__drawer_{item_name}")
462
+ )
463
+
464
+ drawer_panel = Element(
465
+ "View",
466
+ {"background_color": "#FFFFFF", "width": 250},
467
+ menu_items,
468
+ )
469
+ children.insert(0, drawer_panel)
470
+
471
+ return Element("View", {"flex_direction": "row", "flex": 1}, children)
472
+
473
+
474
+ def create_drawer_navigator() -> Any:
475
+ """Create a drawer-based navigator.
476
+
477
+ Returns an object with ``Navigator`` and ``Screen`` members::
478
+
479
+ Drawer = create_drawer_navigator()
480
+
481
+ Drawer.Navigator(
482
+ Drawer.Screen("Home", component=HomeScreen, options={"title": "Home"}),
483
+ Drawer.Screen("Settings", component=SettingsScreen),
484
+ )
485
+
486
+ The navigation handle returned by ``use_navigation()`` inside a drawer
487
+ navigator includes ``open_drawer()``, ``close_drawer()``, and
488
+ ``toggle_drawer()`` methods.
489
+ """
490
+
491
+ class _DrawerNavigator:
492
+ @staticmethod
493
+ def Screen(name: str, *, component: Any, options: Optional[Dict[str, Any]] = None) -> "_ScreenDef":
494
+ """Define a screen within this drawer navigator."""
495
+ return _ScreenDef(name, component, options)
496
+
497
+ @staticmethod
498
+ def Navigator(*screens: Any, initial_route: Optional[str] = None, key: Optional[str] = None) -> Element:
499
+ """Render the drawer navigator with the given screens."""
500
+ return _drawer_navigator_impl(screens=list(screens), initial_route=initial_route, key=key)
501
+
502
+ return _DrawerNavigator()
503
+
504
+
505
+ # ======================================================================
506
+ # NavigationContainer
507
+ # ======================================================================
508
+
509
+
510
+ def NavigationContainer(child: Element, *, key: Optional[str] = None) -> Element:
511
+ """Root container for the navigation tree.
512
+
513
+ Wraps the child navigator in a full-size view. All declarative
514
+ navigators (stack, tab, drawer) should be nested inside a
515
+ ``NavigationContainer``::
516
+
517
+ @pn.component
518
+ def App():
519
+ return NavigationContainer(
520
+ Stack.Navigator(
521
+ Stack.Screen("Home", component=HomeScreen),
522
+ )
523
+ )
524
+ """
525
+ return Element("View", {"flex": 1}, [child], key=key)
526
+
527
+
528
+ # ======================================================================
529
+ # Hooks
530
+ # ======================================================================
531
+
532
+
533
+ def use_route() -> Dict[str, Any]:
534
+ """Return the current route's parameters.
535
+
536
+ Convenience hook that reads from the navigation context::
537
+
538
+ @pn.component
539
+ def DetailScreen():
540
+ params = pn.use_route()
541
+ item_id = params.get("id")
542
+ ...
543
+ """
544
+ nav = use_context(_NavigationContext)
545
+ if nav is None:
546
+ return {}
547
+ get_params = getattr(nav, "get_params", None)
548
+ if get_params:
549
+ return get_params()
550
+ return {}
551
+
552
+
553
+ def use_focus_effect(effect: Callable, deps: Optional[list] = None) -> None:
554
+ """Run *effect* only when the screen is focused.
555
+
556
+ Like ``use_effect`` but skips execution when the screen is not the
557
+ active/focused screen in a navigator::
558
+
559
+ @pn.component
560
+ def HomeScreen():
561
+ pn.use_focus_effect(lambda: print("screen focused"), [])
562
+ """
563
+ is_focused = use_context(_FocusContext)
564
+ all_deps = [is_focused] + (list(deps) if deps is not None else [])
565
+
566
+ def wrapped_effect() -> Any:
567
+ if is_focused:
568
+ return effect()
569
+ return None
570
+
571
+ use_effect(wrapped_effect, all_deps)
pythonnative/page.py CHANGED
@@ -27,7 +27,9 @@ import importlib
27
27
  import json
28
28
  from typing import Any, Dict, Optional
29
29
 
30
- from .utils import IS_ANDROID, set_android_context
30
+ from .utils import IS_ANDROID, IS_IOS, set_android_context
31
+
32
+ _MAX_RENDER_PASSES = 25
31
33
 
32
34
  # ======================================================================
33
35
  # Component path resolution
@@ -62,6 +64,9 @@ def _init_host_common(host: Any) -> None:
62
64
  host._args = {}
63
65
  host._reconciler = None
64
66
  host._root_native_view = None
67
+ host._nav_handle = None
68
+ host._is_rendering = False
69
+ host._render_queued = False
65
70
 
66
71
 
67
72
  def _on_create(host: Any) -> None:
@@ -70,28 +75,68 @@ def _on_create(host: Any) -> None:
70
75
  from .reconciler import Reconciler
71
76
 
72
77
  host._reconciler = Reconciler(get_registry())
73
- host._reconciler._page_re_render = lambda: _re_render(host)
78
+ host._reconciler._page_re_render = lambda: _request_render(host)
79
+ host._nav_handle = NavigationHandle(host)
74
80
 
75
- nav_handle = NavigationHandle(host)
76
81
  app_element = host._component()
77
- provider_element = Provider(_NavigationContext, nav_handle, app_element)
82
+ provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
83
+
84
+ host._is_rendering = True
85
+ try:
86
+ host._root_native_view = host._reconciler.mount(provider_element)
87
+ host._attach_root(host._root_native_view)
88
+ _drain_renders(host)
89
+ finally:
90
+ host._is_rendering = False
78
91
 
79
- host._root_native_view = host._reconciler.mount(provider_element)
80
- host._attach_root(host._root_native_view)
92
+
93
+ def _request_render(host: Any) -> None:
94
+ """State-change trigger. Defers if a render is already in progress."""
95
+ if host._is_rendering:
96
+ host._render_queued = True
97
+ return
98
+ _re_render(host)
81
99
 
82
100
 
83
101
  def _re_render(host: Any) -> None:
84
- from .hooks import NavigationHandle, Provider, _NavigationContext
102
+ """Perform a full render pass, draining any state set during effects."""
103
+ from .hooks import Provider, _NavigationContext
85
104
 
86
- nav_handle = NavigationHandle(host)
87
- app_element = host._component()
88
- provider_element = Provider(_NavigationContext, nav_handle, app_element)
105
+ host._is_rendering = True
106
+ try:
107
+ host._render_queued = False
108
+
109
+ app_element = host._component()
110
+ provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
111
+
112
+ new_root = host._reconciler.reconcile(provider_element)
113
+ if new_root is not host._root_native_view:
114
+ host._detach_root(host._root_native_view)
115
+ host._root_native_view = new_root
116
+ host._attach_root(new_root)
89
117
 
90
- new_root = host._reconciler.reconcile(provider_element)
91
- if new_root is not host._root_native_view:
92
- host._detach_root(host._root_native_view)
93
- host._root_native_view = new_root
94
- host._attach_root(new_root)
118
+ _drain_renders(host)
119
+ finally:
120
+ host._is_rendering = False
121
+
122
+
123
+ def _drain_renders(host: Any) -> None:
124
+ """Flush additional renders queued by effects that set state."""
125
+ from .hooks import Provider, _NavigationContext
126
+
127
+ for _ in range(_MAX_RENDER_PASSES):
128
+ if not host._render_queued:
129
+ break
130
+ host._render_queued = False
131
+
132
+ app_element = host._component()
133
+ provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
134
+
135
+ new_root = host._reconciler.reconcile(provider_element)
136
+ if new_root is not host._root_native_view:
137
+ host._detach_root(host._root_native_view)
138
+ host._root_native_view = new_root
139
+ host._attach_root(new_root)
95
140
 
96
141
 
97
142
  def _set_args(host: Any, args: Any) -> None:
@@ -205,6 +250,21 @@ else:
205
250
  except ImportError:
206
251
  pass
207
252
 
253
+ # Redirect Python's stdout/stderr through fd 2 so ``print()`` output is
254
+ # visible via ``xcrun simctl launch --console-pty``. This runs at
255
+ # ``pythonnative.page`` import time, i.e. before any user page module
256
+ # (e.g. ``app.main_page``) is imported, so their top-level ``print()``
257
+ # calls are captured too. Gated on ``IS_IOS`` rather than rubicon-objc
258
+ # being importable, so installing the ``[ios]`` extra on macOS does
259
+ # not silently swap ``sys.stdout`` on a dev machine.
260
+ if IS_IOS:
261
+ try:
262
+ from . import _ios_log
263
+
264
+ _ios_log.install()
265
+ except Exception:
266
+ pass
267
+
208
268
  _IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
209
269
 
210
270
  def _ios_register_page(vc_instance: Any, host_obj: Any) -> None:
@@ -392,10 +452,10 @@ else:
392
452
  return self._args
393
453
 
394
454
  def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
395
- raise RuntimeError("push() requires a native runtime (iOS or Android)")
455
+ raise RuntimeError("navigate() requires a native runtime (iOS or Android)")
396
456
 
397
457
  def _pop(self) -> None:
398
- raise RuntimeError("pop() requires a native runtime (iOS or Android)")
458
+ raise RuntimeError("go_back() requires a native runtime (iOS or Android)")
399
459
 
400
460
  def _attach_root(self, native_view: Any) -> None:
401
461
  pass