pythonnative 0.32.0__py3-none-any.whl → 0.34.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/__init__.py +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
pythonnative/hooks.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Hook primitives for function components.
|
|
2
2
|
|
|
3
|
-
Provides React-like hooks for managing state, effects, memoization,
|
|
4
|
-
context
|
|
5
|
-
[`component`][pythonnative.component]. Hooks must be called at the top
|
|
3
|
+
Provides React-like hooks for managing state, effects, memoization, and
|
|
4
|
+
context within components decorated with
|
|
5
|
+
[`component`][pythonnative.component.component]. Hooks must be called at the top
|
|
6
6
|
level of a component (not inside conditionals or loops) so they map to
|
|
7
7
|
the same slot across renders. In dev mode the framework verifies this
|
|
8
8
|
and raises [`HookOrderError`][pythonnative.diagnostics.HookOrderError]
|
|
@@ -25,12 +25,17 @@ than a plain global, so ``async def`` component bodies keep their hook
|
|
|
25
25
|
identity across ``await`` boundaries even when several coroutine
|
|
26
26
|
renders interleave on the event loop.
|
|
27
27
|
|
|
28
|
+
Hooks talk to the reconciler through the small
|
|
29
|
+
[`RenderOwner`][pythonnative.hooks.RenderOwner] protocol (mark a
|
|
30
|
+
component dirty, request a render, defer a transition, register a back
|
|
31
|
+
handler). That is the whole contract between the two modules.
|
|
32
|
+
|
|
28
33
|
Example:
|
|
29
34
|
```python
|
|
30
35
|
import pythonnative as pn
|
|
31
36
|
|
|
32
37
|
@pn.component
|
|
33
|
-
def Counter(initial=0):
|
|
38
|
+
def Counter(initial: int = 0):
|
|
34
39
|
count, set_count = pn.use_state(initial)
|
|
35
40
|
return pn.Column(
|
|
36
41
|
pn.Text(f"Count: {count}"),
|
|
@@ -39,36 +44,67 @@ Example:
|
|
|
39
44
|
```
|
|
40
45
|
"""
|
|
41
46
|
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
42
49
|
import asyncio
|
|
43
50
|
import inspect
|
|
44
|
-
from contextlib import contextmanager
|
|
45
51
|
from contextvars import ContextVar, Token
|
|
46
52
|
from dataclasses import dataclass, field, replace
|
|
47
|
-
from typing import
|
|
53
|
+
from typing import (
|
|
54
|
+
Any,
|
|
55
|
+
Awaitable,
|
|
56
|
+
Callable,
|
|
57
|
+
Dict,
|
|
58
|
+
Generic,
|
|
59
|
+
List,
|
|
60
|
+
Optional,
|
|
61
|
+
Protocol,
|
|
62
|
+
Tuple,
|
|
63
|
+
TypeVar,
|
|
64
|
+
Union,
|
|
65
|
+
overload,
|
|
66
|
+
)
|
|
48
67
|
|
|
49
68
|
from . import diagnostics
|
|
50
|
-
from .element import Element
|
|
69
|
+
from .element import Element, Node
|
|
70
|
+
from .scheduler import TransitionQueue, in_transition, run_in_transition, schedule_trigger
|
|
51
71
|
from .suspense import CoroDriver, Resource
|
|
52
72
|
|
|
53
73
|
T = TypeVar("T")
|
|
54
74
|
|
|
75
|
+
StateSetter = Callable[[Union[T, Callable[[T], T]]], None]
|
|
76
|
+
"""Setter returned by [`use_state`][pythonnative.use_state]: accepts a value or ``current -> new``."""
|
|
77
|
+
|
|
55
78
|
_SENTINEL = object()
|
|
56
79
|
|
|
57
80
|
# The component whose body is currently executing. A ContextVar (not a
|
|
58
81
|
# global or thread-local) so coroutine component bodies resume with the
|
|
59
82
|
# right hook state after every ``await``, no matter how renders
|
|
60
83
|
# interleave on the loop.
|
|
61
|
-
_hook_context:
|
|
84
|
+
_hook_context: ContextVar[Optional["HookState"]] = ContextVar("pn_hook_state", default=None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ======================================================================
|
|
88
|
+
# Reconciler contract
|
|
89
|
+
# ======================================================================
|
|
90
|
+
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
class RenderOwner(Protocol):
|
|
93
|
+
"""What a hook needs from the object that renders its component.
|
|
94
|
+
|
|
95
|
+
The reconciler implements this; tests may substitute a stub.
|
|
96
|
+
"""
|
|
67
97
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
transitions: TransitionQueue
|
|
99
|
+
|
|
100
|
+
def mark_dirty(self, vnode: Any) -> None:
|
|
101
|
+
"""Queue ``vnode``'s component for a local re-render."""
|
|
102
|
+
|
|
103
|
+
def request_render(self) -> None:
|
|
104
|
+
"""Ask the host to flush dirty components (may be deferred)."""
|
|
105
|
+
|
|
106
|
+
def register_back_handler(self, handler: Callable[[], bool]) -> Callable[[], None]:
|
|
107
|
+
"""Register a system back-press handler; returns an unregister callable."""
|
|
72
108
|
|
|
73
109
|
|
|
74
110
|
# ======================================================================
|
|
@@ -120,7 +156,7 @@ class Ref(Generic[T]):
|
|
|
120
156
|
class HookState:
|
|
121
157
|
"""Per-instance storage for one component's hooks.
|
|
122
158
|
|
|
123
|
-
Each
|
|
159
|
+
Each component instance owns one ``HookState``. Hooks are matched
|
|
124
160
|
to slots by call order, so they must always be called in the same
|
|
125
161
|
order across renders. Effects scheduled during render are deferred
|
|
126
162
|
(layout effects into ``_pending_layout_effects``, passive effects
|
|
@@ -128,12 +164,15 @@ class HookState:
|
|
|
128
164
|
phases after native mutations commit.
|
|
129
165
|
|
|
130
166
|
Attributes:
|
|
131
|
-
states: One entry per
|
|
132
|
-
effects: One
|
|
133
|
-
layout_effects: One
|
|
134
|
-
|
|
135
|
-
memos: One
|
|
136
|
-
refs: One [`Ref`][pythonnative.Ref] per
|
|
167
|
+
states: One entry per ``use_state`` / ``use_reducer`` call.
|
|
168
|
+
effects: One ``(deps, cleanup)`` tuple per ``use_effect`` call.
|
|
169
|
+
layout_effects: One ``(deps, cleanup)`` tuple per
|
|
170
|
+
``use_layout_effect`` call.
|
|
171
|
+
memos: One ``(deps, value)`` tuple per ``use_memo`` / ``use_callback``.
|
|
172
|
+
refs: One [`Ref`][pythonnative.Ref] per ``use_ref`` call.
|
|
173
|
+
owner: The [`RenderOwner`][pythonnative.hooks.RenderOwner]
|
|
174
|
+
(reconciler) this component is mounted in, or ``None``.
|
|
175
|
+
vnode: The reconciler's node for this component, or ``None``.
|
|
137
176
|
"""
|
|
138
177
|
|
|
139
178
|
__slots__ = (
|
|
@@ -150,14 +189,13 @@ class HookState:
|
|
|
150
189
|
"ref_index",
|
|
151
190
|
"resource_index",
|
|
152
191
|
"context_deps",
|
|
153
|
-
"
|
|
192
|
+
"owner",
|
|
193
|
+
"vnode",
|
|
154
194
|
"_pending_effects",
|
|
155
195
|
"_pending_layout_effects",
|
|
156
196
|
"_pending_effects_mark",
|
|
157
197
|
"_pending_layout_effects_mark",
|
|
158
198
|
"_dirty",
|
|
159
|
-
"_vnode",
|
|
160
|
-
"_reconciler",
|
|
161
199
|
"_hook_log",
|
|
162
200
|
"_hook_signature",
|
|
163
201
|
"_component_name",
|
|
@@ -183,22 +221,15 @@ class HookState:
|
|
|
183
221
|
# Provider's value changes so consumers re-render even when a
|
|
184
222
|
# memoized ancestor skipped (reactive context).
|
|
185
223
|
self.context_deps: Dict[int, Any] = {}
|
|
186
|
-
self.
|
|
224
|
+
self.owner: Optional[RenderOwner] = None
|
|
225
|
+
self.vnode: Any = None
|
|
187
226
|
self._pending_effects: List[Tuple[int, Callable, Any]] = []
|
|
188
227
|
self._pending_layout_effects: List[Tuple[int, Callable, Any]] = []
|
|
189
228
|
# Cleared by the reconciler after each successful render.
|
|
190
229
|
# ``use_state`` / ``use_reducer`` setters flip it to ``True``
|
|
191
|
-
# whenever they actually mutate state, so
|
|
192
|
-
#
|
|
193
|
-
# when its props didn't change.
|
|
230
|
+
# whenever they actually mutate state, so a memoized component
|
|
231
|
+
# still re-renders even when its props didn't change.
|
|
194
232
|
self._dirty: bool = False
|
|
195
|
-
# Back-references wired by the reconciler so a state setter can
|
|
196
|
-
# mark *its own* component subtree dirty for a local re-render
|
|
197
|
-
# (instead of forcing a whole-app re-render from the root). Both
|
|
198
|
-
# stay ``None`` until the component is mounted, and are cleared
|
|
199
|
-
# again when it unmounts.
|
|
200
|
-
self._vnode: Any = None
|
|
201
|
-
self._reconciler: Any = None
|
|
202
233
|
# Dev-mode hook-order guard: the sequence of hook kinds called
|
|
203
234
|
# during the in-flight render, and the signature captured from
|
|
204
235
|
# the first successful render.
|
|
@@ -213,13 +244,12 @@ class HookState:
|
|
|
213
244
|
self._pending_effects_mark: int = 0
|
|
214
245
|
self._pending_layout_effects_mark: int = 0
|
|
215
246
|
|
|
216
|
-
|
|
217
|
-
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
# Render lifecycle
|
|
249
|
+
# ------------------------------------------------------------------
|
|
218
250
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
order they were written.
|
|
222
|
-
"""
|
|
251
|
+
def begin_render(self, component_name: str = "") -> None:
|
|
252
|
+
"""Prepare for a render pass: reset cursors and the dev-mode hook log."""
|
|
223
253
|
self.state_index = 0
|
|
224
254
|
self.effect_index = 0
|
|
225
255
|
self.layout_effect_index = 0
|
|
@@ -246,9 +276,6 @@ class HookState:
|
|
|
246
276
|
def finish_render(self) -> None:
|
|
247
277
|
"""Finalize a successful render: lock in / verify the hook signature.
|
|
248
278
|
|
|
249
|
-
Only called when the component body returned without raising,
|
|
250
|
-
so a failed render never corrupts the signature.
|
|
251
|
-
|
|
252
279
|
Raises:
|
|
253
280
|
HookOrderError: In dev mode, when this render called fewer
|
|
254
281
|
hooks than the previous one.
|
|
@@ -297,14 +324,13 @@ class HookState:
|
|
|
297
324
|
)
|
|
298
325
|
|
|
299
326
|
def reset_hook_signature(self) -> None:
|
|
300
|
-
"""Forget the recorded hook signature (used by Fast Refresh).
|
|
301
|
-
|
|
302
|
-
After a hot reload swaps in a new component body, the old
|
|
303
|
-
signature no longer applies; the next render records a fresh
|
|
304
|
-
one.
|
|
305
|
-
"""
|
|
327
|
+
"""Forget the recorded hook signature (used by Fast Refresh)."""
|
|
306
328
|
self._hook_signature = None
|
|
307
329
|
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
# Effects
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
|
|
308
334
|
def flush_layout_effects(self) -> None:
|
|
309
335
|
"""Run layout effects queued during render (commit phase, pre-paint)."""
|
|
310
336
|
pending = self._pending_layout_effects
|
|
@@ -312,11 +338,7 @@ class HookState:
|
|
|
312
338
|
self._pending_layout_effects_mark = 0
|
|
313
339
|
for idx, effect_fn, deps in pending:
|
|
314
340
|
_, prev_cleanup = self.layout_effects[idx]
|
|
315
|
-
|
|
316
|
-
try:
|
|
317
|
-
prev_cleanup()
|
|
318
|
-
except Exception:
|
|
319
|
-
pass
|
|
341
|
+
_run_cleanup(prev_cleanup)
|
|
320
342
|
cleanup = _activate_effect(effect_fn)
|
|
321
343
|
self.layout_effects[idx] = (list(deps) if deps is not None else None, cleanup)
|
|
322
344
|
|
|
@@ -335,11 +357,7 @@ class HookState:
|
|
|
335
357
|
self._pending_effects_mark = 0
|
|
336
358
|
for idx, effect_fn, deps in pending:
|
|
337
359
|
_, prev_cleanup = self.effects[idx]
|
|
338
|
-
|
|
339
|
-
try:
|
|
340
|
-
prev_cleanup()
|
|
341
|
-
except Exception:
|
|
342
|
-
pass
|
|
360
|
+
_run_cleanup(prev_cleanup)
|
|
343
361
|
cleanup = _activate_effect(effect_fn)
|
|
344
362
|
self.effects[idx] = (list(deps) if deps is not None else None, cleanup)
|
|
345
363
|
|
|
@@ -352,18 +370,10 @@ class HookState:
|
|
|
352
370
|
component instance is unmounted by the reconciler.
|
|
353
371
|
"""
|
|
354
372
|
for i, (_deps, cleanup) in enumerate(self.layout_effects):
|
|
355
|
-
|
|
356
|
-
try:
|
|
357
|
-
cleanup()
|
|
358
|
-
except Exception:
|
|
359
|
-
pass
|
|
373
|
+
_run_cleanup(cleanup)
|
|
360
374
|
self.layout_effects[i] = (_SENTINEL, None)
|
|
361
375
|
for i, (_deps, cleanup) in enumerate(self.effects):
|
|
362
|
-
|
|
363
|
-
try:
|
|
364
|
-
cleanup()
|
|
365
|
-
except Exception:
|
|
366
|
-
pass
|
|
376
|
+
_run_cleanup(cleanup)
|
|
367
377
|
self.effects[i] = (_SENTINEL, None)
|
|
368
378
|
self._pending_effects = []
|
|
369
379
|
self._pending_layout_effects = []
|
|
@@ -380,27 +390,44 @@ class HookState:
|
|
|
380
390
|
if driver is not None:
|
|
381
391
|
driver.cancel()
|
|
382
392
|
|
|
393
|
+
def detach(self) -> None:
|
|
394
|
+
"""Break the back-references to the reconciler (on unmount).
|
|
395
|
+
|
|
396
|
+
Lets the unmounted component's hook state (and the closures it
|
|
397
|
+
captured) be freed by plain refcounting, which matters on iOS
|
|
398
|
+
where the cyclic GC is disabled.
|
|
399
|
+
"""
|
|
400
|
+
self.owner = None
|
|
401
|
+
self.vnode = None
|
|
402
|
+
|
|
383
403
|
|
|
384
404
|
# ======================================================================
|
|
385
|
-
# Context helpers
|
|
405
|
+
# Context helpers (framework-internal)
|
|
386
406
|
# ======================================================================
|
|
387
407
|
|
|
388
408
|
|
|
389
|
-
def
|
|
390
|
-
"""Return the active
|
|
409
|
+
def current_hook_state() -> Optional[HookState]:
|
|
410
|
+
"""Return the active ``HookState``, or ``None`` if no render is in flight."""
|
|
391
411
|
return _hook_context.get()
|
|
392
412
|
|
|
393
413
|
|
|
394
|
-
def
|
|
395
|
-
"""Install
|
|
414
|
+
def install_hook_state(state: Optional[HookState]) -> Token[Optional[HookState]]:
|
|
415
|
+
"""Install ``state`` as the active ``HookState``; returns the reset token."""
|
|
396
416
|
return _hook_context.set(state)
|
|
397
417
|
|
|
398
418
|
|
|
399
|
-
def
|
|
400
|
-
"""Restore the hook state that was active before
|
|
419
|
+
def restore_hook_state(token: Token[Optional[HookState]]) -> None:
|
|
420
|
+
"""Restore the hook state that was active before ``install_hook_state``."""
|
|
401
421
|
_hook_context.reset(token)
|
|
402
422
|
|
|
403
423
|
|
|
424
|
+
def _require_hook_state(hook_name: str) -> HookState:
|
|
425
|
+
ctx = _hook_context.get()
|
|
426
|
+
if ctx is None:
|
|
427
|
+
raise RuntimeError(f"{hook_name} must be called inside a @component function")
|
|
428
|
+
return ctx
|
|
429
|
+
|
|
430
|
+
|
|
404
431
|
def _deps_changed(prev: Any, current: Any) -> bool:
|
|
405
432
|
"""Return whether the dependency arrays differ enough to re-run an effect."""
|
|
406
433
|
if prev is _SENTINEL:
|
|
@@ -412,6 +439,17 @@ def _deps_changed(prev: Any, current: Any) -> bool:
|
|
|
412
439
|
return any(p is not c and p != c for p, c in zip(prev, current))
|
|
413
440
|
|
|
414
441
|
|
|
442
|
+
def _run_cleanup(cleanup: Any) -> None:
|
|
443
|
+
if callable(cleanup):
|
|
444
|
+
try:
|
|
445
|
+
cleanup()
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
# Never let a failing cleanup abort an unmount; surface it
|
|
448
|
+
# through the RedBox in dev mode, a warning otherwise.
|
|
449
|
+
if not diagnostics.report_error(exc, phase="effect cleanup"):
|
|
450
|
+
diagnostics.warn(f"Effect cleanup raised {exc!r}")
|
|
451
|
+
|
|
452
|
+
|
|
415
453
|
def _activate_effect(effect_fn: Callable) -> Any:
|
|
416
454
|
"""Invoke an effect callback, running coroutine effects as tasks.
|
|
417
455
|
|
|
@@ -462,143 +500,47 @@ def _activate_effect(effect_fn: Callable) -> Any:
|
|
|
462
500
|
return _cleanup
|
|
463
501
|
|
|
464
502
|
|
|
465
|
-
|
|
466
|
-
# Batching and transition scheduling
|
|
467
|
-
# ======================================================================
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def _schedule_trigger(trigger: Callable[[], None]) -> None:
|
|
471
|
-
"""Run ``trigger`` immediately, or defer it inside a `batch_updates` block."""
|
|
472
|
-
pending = _batch_pending.get()
|
|
473
|
-
if _batch_depth.get() > 0 and pending is not None:
|
|
474
|
-
if trigger not in pending:
|
|
475
|
-
pending.append(trigger)
|
|
476
|
-
else:
|
|
477
|
-
trigger()
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
# Transition scheduling state. Deferred triggers (and the callbacks
|
|
481
|
-
# that flip ``is_pending`` back off) run together on a later loop turn.
|
|
482
|
-
_deferred_triggers: List[Callable[[], None]] = []
|
|
483
|
-
_post_transition_callbacks: List[Callable[[], None]] = []
|
|
484
|
-
_transition_flush_scheduled = False
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def _flush_transitions() -> None:
|
|
488
|
-
"""Run every deferred transition render, then the completion callbacks."""
|
|
489
|
-
global _transition_flush_scheduled
|
|
490
|
-
_transition_flush_scheduled = False
|
|
491
|
-
triggers = list(_deferred_triggers)
|
|
492
|
-
_deferred_triggers.clear()
|
|
493
|
-
callbacks = list(_post_transition_callbacks)
|
|
494
|
-
_post_transition_callbacks.clear()
|
|
495
|
-
for trigger in triggers:
|
|
496
|
-
try:
|
|
497
|
-
trigger()
|
|
498
|
-
except Exception as exc:
|
|
499
|
-
if not diagnostics.report_error(exc, phase="transition"):
|
|
500
|
-
raise
|
|
501
|
-
for callback in callbacks:
|
|
502
|
-
try:
|
|
503
|
-
callback()
|
|
504
|
-
except Exception as exc:
|
|
505
|
-
if not diagnostics.report_error(exc, phase="transition"):
|
|
506
|
-
raise
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def _schedule_transition_flush() -> None:
|
|
510
|
-
"""Ensure a transition flush is queued on the framework loop."""
|
|
511
|
-
global _transition_flush_scheduled
|
|
512
|
-
if _transition_flush_scheduled:
|
|
513
|
-
return
|
|
514
|
-
_transition_flush_scheduled = True
|
|
515
|
-
from .runtime import get_loop
|
|
516
|
-
|
|
517
|
-
get_loop().call_soon(_flush_transitions)
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
def _defer_transition_trigger(trigger: Callable[[], None]) -> None:
|
|
521
|
-
if trigger not in _deferred_triggers:
|
|
522
|
-
_deferred_triggers.append(trigger)
|
|
523
|
-
_schedule_transition_flush()
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
def _run_in_transition(fn: Callable[[], None]) -> None:
|
|
527
|
-
"""Run ``fn`` with its state updates marked as transitions."""
|
|
528
|
-
token = _transition_var.set(True)
|
|
529
|
-
try:
|
|
530
|
-
fn()
|
|
531
|
-
finally:
|
|
532
|
-
_transition_var.reset(token)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def _notify_state_changed(ctx: "HookState") -> None:
|
|
503
|
+
def _notify_state_changed(ctx: HookState) -> None:
|
|
536
504
|
"""Mark ``ctx``'s component dirty and schedule a render after a state change.
|
|
537
505
|
|
|
538
|
-
Enqueuing the owning
|
|
539
|
-
makes the subsequent render *local*: the
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
loop turn inside a transition (see
|
|
506
|
+
Enqueuing the owning node in the reconciler's dirty set is what
|
|
507
|
+
makes the subsequent render *local*: the host's trigger flushes
|
|
508
|
+
only the components marked here rather than the whole app. The
|
|
509
|
+
dirty mark is eager (so several setters coalesce), while the render
|
|
510
|
+
request respects [`batch_updates`][pythonnative.scheduler.batch_updates] and
|
|
511
|
+
defers to a later loop turn inside a transition (see
|
|
545
512
|
[`use_transition`][pythonnative.use_transition]).
|
|
546
513
|
"""
|
|
547
514
|
ctx._dirty = True
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
515
|
+
owner = ctx.owner
|
|
516
|
+
if owner is None:
|
|
517
|
+
return
|
|
518
|
+
if ctx.vnode is not None:
|
|
519
|
+
owner.mark_dirty(ctx.vnode)
|
|
520
|
+
if in_transition():
|
|
521
|
+
owner.transitions.defer(owner.request_render)
|
|
522
|
+
else:
|
|
523
|
+
schedule_trigger(owner.request_render)
|
|
557
524
|
|
|
558
525
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
526
|
+
# ======================================================================
|
|
527
|
+
# State hooks
|
|
528
|
+
# ======================================================================
|
|
562
529
|
|
|
563
|
-
State setters called inside the `with` block defer their
|
|
564
|
-
re-render trigger until the block exits, so any number of
|
|
565
|
-
`set_*` calls produce at most one render pass.
|
|
566
530
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
exit.
|
|
531
|
+
@overload
|
|
532
|
+
def use_state() -> Tuple[Optional[Any], StateSetter[Any]]: ...
|
|
570
533
|
|
|
571
|
-
Example:
|
|
572
|
-
```python
|
|
573
|
-
import pythonnative as pn
|
|
574
534
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
set_name("hello")
|
|
578
|
-
```
|
|
579
|
-
"""
|
|
580
|
-
depth = _batch_depth.get()
|
|
581
|
-
depth_token = _batch_depth.set(depth + 1)
|
|
582
|
-
pending_token = None
|
|
583
|
-
if depth == 0:
|
|
584
|
-
pending_token = _batch_pending.set([])
|
|
585
|
-
try:
|
|
586
|
-
yield
|
|
587
|
-
finally:
|
|
588
|
-
_batch_depth.reset(depth_token)
|
|
589
|
-
if pending_token is not None:
|
|
590
|
-
triggers = _batch_pending.get() or []
|
|
591
|
-
_batch_pending.reset(pending_token)
|
|
592
|
-
for trigger in triggers:
|
|
593
|
-
trigger()
|
|
535
|
+
@overload
|
|
536
|
+
def use_state(initial: Callable[[], T]) -> Tuple[T, StateSetter[T]]: ...
|
|
594
537
|
|
|
595
538
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# ======================================================================
|
|
539
|
+
@overload
|
|
540
|
+
def use_state(initial: T) -> Tuple[T, StateSetter[T]]: ...
|
|
599
541
|
|
|
600
542
|
|
|
601
|
-
def use_state(initial: Any = None) -> Tuple[Any,
|
|
543
|
+
def use_state(initial: Any = None) -> Tuple[Any, StateSetter[Any]]:
|
|
602
544
|
"""Return ``(value, setter)`` for component-local state.
|
|
603
545
|
|
|
604
546
|
State persists across re-renders of the same component instance.
|
|
@@ -614,7 +556,7 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
614
556
|
state and ``setter`` updates it (and triggers a re-render).
|
|
615
557
|
|
|
616
558
|
Raises:
|
|
617
|
-
RuntimeError: If called outside a
|
|
559
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
618
560
|
|
|
619
561
|
Example:
|
|
620
562
|
```python
|
|
@@ -629,9 +571,7 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
629
571
|
)
|
|
630
572
|
```
|
|
631
573
|
"""
|
|
632
|
-
ctx =
|
|
633
|
-
if ctx is None:
|
|
634
|
-
raise RuntimeError("use_state must be called inside a @component function")
|
|
574
|
+
ctx = _require_hook_state("use_state")
|
|
635
575
|
ctx.record_hook("use_state")
|
|
636
576
|
|
|
637
577
|
idx = ctx.state_index
|
|
@@ -653,7 +593,9 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
653
593
|
return current, setter
|
|
654
594
|
|
|
655
595
|
|
|
656
|
-
def use_reducer(
|
|
596
|
+
def use_reducer(
|
|
597
|
+
reducer: Callable[[T, Any], T], initial_state: Union[T, Callable[[], T]]
|
|
598
|
+
) -> Tuple[T, Callable[[Any], None]]:
|
|
657
599
|
"""Return ``(state, dispatch)`` for reducer-based state management.
|
|
658
600
|
|
|
659
601
|
A reducer is a pure function that takes the current state and an
|
|
@@ -664,41 +606,19 @@ def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple
|
|
|
664
606
|
|
|
665
607
|
Args:
|
|
666
608
|
reducer: ``reducer(current_state, action) -> new_state``.
|
|
667
|
-
The component re-renders only when
|
|
609
|
+
The component re-renders only when ``reducer`` returns a
|
|
668
610
|
value different from the current state.
|
|
669
611
|
initial_state: Initial state value, or a callable invoked once
|
|
670
612
|
on the first render.
|
|
671
613
|
|
|
672
614
|
Returns:
|
|
673
|
-
A 2-tuple ``(state, dispatch)`` where
|
|
615
|
+
A 2-tuple ``(state, dispatch)`` where ``dispatch`` runs the
|
|
674
616
|
reducer with the supplied action.
|
|
675
617
|
|
|
676
618
|
Raises:
|
|
677
|
-
RuntimeError: If called outside a
|
|
678
|
-
|
|
679
|
-
Example:
|
|
680
|
-
```python
|
|
681
|
-
import pythonnative as pn
|
|
682
|
-
|
|
683
|
-
def reducer(state, action):
|
|
684
|
-
if action == "increment":
|
|
685
|
-
return state + 1
|
|
686
|
-
if action == "reset":
|
|
687
|
-
return 0
|
|
688
|
-
return state
|
|
689
|
-
|
|
690
|
-
@pn.component
|
|
691
|
-
def Counter():
|
|
692
|
-
count, dispatch = pn.use_reducer(reducer, 0)
|
|
693
|
-
return pn.Row(
|
|
694
|
-
pn.Button("+", on_press=lambda: dispatch("increment")),
|
|
695
|
-
pn.Button("Reset", on_press=lambda: dispatch("reset")),
|
|
696
|
-
)
|
|
697
|
-
```
|
|
619
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
698
620
|
"""
|
|
699
|
-
ctx =
|
|
700
|
-
if ctx is None:
|
|
701
|
-
raise RuntimeError("use_reducer must be called inside a @component function")
|
|
621
|
+
ctx = _require_hook_state("use_reducer")
|
|
702
622
|
ctx.record_hook("use_reducer")
|
|
703
623
|
|
|
704
624
|
idx = ctx.state_index
|
|
@@ -719,7 +639,12 @@ def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple
|
|
|
719
639
|
return current, dispatch
|
|
720
640
|
|
|
721
641
|
|
|
722
|
-
|
|
642
|
+
# ======================================================================
|
|
643
|
+
# Effect hooks
|
|
644
|
+
# ======================================================================
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def use_effect(effect: Callable[[], Any], deps: Optional[list] = None) -> None:
|
|
723
648
|
"""Schedule a side effect to run after the native commit.
|
|
724
649
|
|
|
725
650
|
Effects are queued during the render pass and flushed once the
|
|
@@ -727,17 +652,17 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
727
652
|
means effect callbacks can safely measure layout or interact with
|
|
728
653
|
committed native views.
|
|
729
654
|
|
|
730
|
-
The
|
|
655
|
+
The ``deps`` argument controls when the effect re-runs:
|
|
731
656
|
|
|
732
|
-
-
|
|
733
|
-
-
|
|
734
|
-
-
|
|
657
|
+
- ``None``: every render.
|
|
658
|
+
- ``[]``: mount only.
|
|
659
|
+
- ``[a, b]``: when ``a`` or ``b`` change (compared by identity, then ``==``).
|
|
735
660
|
|
|
736
|
-
A synchronous
|
|
661
|
+
A synchronous ``effect`` may return a cleanup callable; the previous
|
|
737
662
|
cleanup runs before the next effect (and on unmount).
|
|
738
663
|
|
|
739
|
-
An **async**
|
|
740
|
-
framework loop. When
|
|
664
|
+
An **async** ``effect`` (an ``async def``) runs as a task on the
|
|
665
|
+
framework loop. When ``deps`` change or the component unmounts, the
|
|
741
666
|
in-flight task is cancelled (:class:`asyncio.CancelledError` is
|
|
742
667
|
raised at its current ``await``), giving async effects structured
|
|
743
668
|
cancellation for free. If the coroutine finishes and returns a
|
|
@@ -747,14 +672,15 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
747
672
|
effect: A zero-arg callable invoked after commit: either a
|
|
748
673
|
synchronous function (optionally returning a cleanup
|
|
749
674
|
callable) or an ``async def``.
|
|
750
|
-
deps: Dependency list, or
|
|
675
|
+
deps: Dependency list, or ``None`` to run on every render.
|
|
751
676
|
|
|
752
677
|
Raises:
|
|
753
|
-
RuntimeError: If called outside a
|
|
678
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
754
679
|
|
|
755
680
|
Example:
|
|
756
681
|
```python
|
|
757
682
|
import asyncio
|
|
683
|
+
import time
|
|
758
684
|
|
|
759
685
|
import pythonnative as pn
|
|
760
686
|
|
|
@@ -771,9 +697,7 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
771
697
|
return pn.Text(now)
|
|
772
698
|
```
|
|
773
699
|
"""
|
|
774
|
-
ctx =
|
|
775
|
-
if ctx is None:
|
|
776
|
-
raise RuntimeError("use_effect must be called inside a @component function")
|
|
700
|
+
ctx = _require_hook_state("use_effect")
|
|
777
701
|
ctx.record_hook("use_effect")
|
|
778
702
|
|
|
779
703
|
idx = ctx.effect_index
|
|
@@ -789,7 +713,7 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
789
713
|
ctx._pending_effects.append((idx, effect, deps))
|
|
790
714
|
|
|
791
715
|
|
|
792
|
-
def use_layout_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
716
|
+
def use_layout_effect(effect: Callable[[], Any], deps: Optional[list] = None) -> None:
|
|
793
717
|
"""Schedule a side effect that runs synchronously inside the commit.
|
|
794
718
|
|
|
795
719
|
Like [`use_effect`][pythonnative.use_effect], but the callback
|
|
@@ -799,36 +723,18 @@ def use_layout_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
799
723
|
imperative view command before the user sees the new frame, for
|
|
800
724
|
example scrolling a list into position on mount.
|
|
801
725
|
|
|
802
|
-
Prefer
|
|
726
|
+
Prefer ``use_effect`` for everything else; layout effects block the
|
|
803
727
|
commit, so heavy work here delays the frame.
|
|
804
728
|
|
|
805
729
|
Args:
|
|
806
730
|
effect: A zero-arg callable invoked during commit. Optionally
|
|
807
731
|
returns a cleanup callable.
|
|
808
|
-
deps: Dependency list, or
|
|
732
|
+
deps: Dependency list, or ``None`` to run on every render.
|
|
809
733
|
|
|
810
734
|
Raises:
|
|
811
|
-
RuntimeError: If called outside a
|
|
812
|
-
|
|
813
|
-
Example:
|
|
814
|
-
```python
|
|
815
|
-
import pythonnative as pn
|
|
816
|
-
|
|
817
|
-
@pn.component
|
|
818
|
-
def AutoScrollList(items):
|
|
819
|
-
list_ref = pn.use_ref()
|
|
820
|
-
|
|
821
|
-
def scroll_to_bottom():
|
|
822
|
-
if list_ref.current is not None:
|
|
823
|
-
list_ref.current.scroll_to_end(animated=False)
|
|
824
|
-
|
|
825
|
-
pn.use_layout_effect(scroll_to_bottom, [len(items)])
|
|
826
|
-
return pn.FlatList(items, render_item=Row, ref=list_ref)
|
|
827
|
-
```
|
|
735
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
828
736
|
"""
|
|
829
|
-
ctx =
|
|
830
|
-
if ctx is None:
|
|
831
|
-
raise RuntimeError("use_layout_effect must be called inside a @component function")
|
|
737
|
+
ctx = _require_hook_state("use_layout_effect")
|
|
832
738
|
ctx.record_hook("use_layout_effect")
|
|
833
739
|
|
|
834
740
|
idx = ctx.layout_effect_index
|
|
@@ -844,8 +750,13 @@ def use_layout_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
844
750
|
ctx._pending_layout_effects.append((idx, effect, deps))
|
|
845
751
|
|
|
846
752
|
|
|
753
|
+
# ======================================================================
|
|
754
|
+
# Memoization hooks
|
|
755
|
+
# ======================================================================
|
|
756
|
+
|
|
757
|
+
|
|
847
758
|
def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
848
|
-
"""Return a memoized value that is recomputed only when
|
|
759
|
+
"""Return a memoized value that is recomputed only when ``deps`` change.
|
|
849
760
|
|
|
850
761
|
Use this for expensive computations whose inputs change rarely. For
|
|
851
762
|
cheap computations, plain inline code is faster (memoization itself
|
|
@@ -860,11 +771,9 @@ def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
|
860
771
|
The cached or freshly computed value.
|
|
861
772
|
|
|
862
773
|
Raises:
|
|
863
|
-
RuntimeError: If called outside a
|
|
774
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
864
775
|
"""
|
|
865
|
-
ctx =
|
|
866
|
-
if ctx is None:
|
|
867
|
-
raise RuntimeError("use_memo must be called inside a @component function")
|
|
776
|
+
ctx = _require_hook_state("use_memo")
|
|
868
777
|
ctx.record_hook("use_memo")
|
|
869
778
|
|
|
870
779
|
idx = ctx.memo_index
|
|
@@ -884,19 +793,22 @@ def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
|
884
793
|
return value
|
|
885
794
|
|
|
886
795
|
|
|
887
|
-
|
|
796
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def use_callback(callback: F, deps: list) -> F:
|
|
888
800
|
"""Return a stable reference to ``callback``, refreshed when ``deps`` change.
|
|
889
801
|
|
|
890
|
-
Equivalent to
|
|
891
|
-
a function as a prop to a memoized child component, so the
|
|
892
|
-
doesn't see a fresh function identity on every render.
|
|
802
|
+
Equivalent to ``use_memo(lambda: callback, deps)``. Useful when
|
|
803
|
+
passing a function as a prop to a memoized child component, so the
|
|
804
|
+
child doesn't see a fresh function identity on every render.
|
|
893
805
|
|
|
894
806
|
Args:
|
|
895
807
|
callback: The callable to memoize.
|
|
896
808
|
deps: Dependency list controlling when the reference refreshes.
|
|
897
809
|
|
|
898
810
|
Returns:
|
|
899
|
-
A callable with stable identity across renders (until
|
|
811
|
+
A callable with stable identity across renders (until ``deps`` change).
|
|
900
812
|
"""
|
|
901
813
|
return use_memo(lambda: callback, deps)
|
|
902
814
|
|
|
@@ -908,10 +820,9 @@ def use_ref(initial: Optional[T] = None) -> Ref[T]:
|
|
|
908
820
|
triggering them: timers, last-seen values, native handles, and so on.
|
|
909
821
|
|
|
910
822
|
``ref.current`` is also populated by the reconciler with the
|
|
911
|
-
underlying native view
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
that element unmounts. Composite components such as
|
|
823
|
+
underlying native view when the ref is passed via the ``ref=`` prop
|
|
824
|
+
on a built-in element, and cleared to ``None`` when that element
|
|
825
|
+
unmounts. Composite components such as
|
|
915
826
|
[`FlatList`][pythonnative.FlatList] publish a typed controller
|
|
916
827
|
object instead (see
|
|
917
828
|
[`use_imperative_handle`][pythonnative.use_imperative_handle]).
|
|
@@ -924,11 +835,9 @@ def use_ref(initial: Optional[T] = None) -> Ref[T]:
|
|
|
924
835
|
*not* trigger re-renders.
|
|
925
836
|
|
|
926
837
|
Raises:
|
|
927
|
-
RuntimeError: If called outside a
|
|
838
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
928
839
|
"""
|
|
929
|
-
ctx =
|
|
930
|
-
if ctx is None:
|
|
931
|
-
raise RuntimeError("use_ref must be called inside a @component function")
|
|
840
|
+
ctx = _require_hook_state("use_ref")
|
|
932
841
|
ctx.record_hook("use_ref")
|
|
933
842
|
|
|
934
843
|
idx = ctx.ref_index
|
|
@@ -943,7 +852,7 @@ def use_ref(initial: Optional[T] = None) -> Ref[T]:
|
|
|
943
852
|
|
|
944
853
|
|
|
945
854
|
def use_imperative_handle(
|
|
946
|
-
ref: Optional[Ref],
|
|
855
|
+
ref: Optional[Ref[Any]],
|
|
947
856
|
factory: Callable[[], Any],
|
|
948
857
|
deps: Optional[list] = None,
|
|
949
858
|
) -> None:
|
|
@@ -962,18 +871,15 @@ def use_imperative_handle(
|
|
|
962
871
|
request a handle), in which case this is a no-op.
|
|
963
872
|
factory: Zero-arg callable returning the handle object.
|
|
964
873
|
deps: Dependency list controlling when the handle is rebuilt.
|
|
965
|
-
Defaults to ``[]`` semantics only if you pass ``[]``;
|
|
966
874
|
``None`` rebuilds on every render, matching effects.
|
|
967
875
|
|
|
968
876
|
Raises:
|
|
969
|
-
RuntimeError: If called outside a
|
|
877
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
970
878
|
|
|
971
879
|
Example:
|
|
972
880
|
```python
|
|
973
|
-
import pythonnative as pn
|
|
974
|
-
|
|
975
881
|
@pn.component
|
|
976
|
-
def VideoPlayer(source, ref=None):
|
|
882
|
+
def VideoPlayer(source: str, ref: pn.Ref | None = None):
|
|
977
883
|
pn.use_imperative_handle(ref, lambda: PlayerController(...), [source])
|
|
978
884
|
return pn.View(...)
|
|
979
885
|
```
|
|
@@ -1028,20 +934,15 @@ def use_resource(fetcher: Callable[[], Any], deps: Optional[list] = None) -> Res
|
|
|
1028
934
|
|
|
1029
935
|
Example:
|
|
1030
936
|
```python
|
|
1031
|
-
import pythonnative as pn
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
937
|
@pn.component
|
|
1035
|
-
async def UserCard(user_id):
|
|
938
|
+
async def UserCard(user_id: str):
|
|
1036
939
|
user = await pn.use_resource(lambda: api.get_user(user_id), [user_id])
|
|
1037
940
|
return pn.Text(user["name"])
|
|
1038
941
|
```
|
|
1039
942
|
"""
|
|
1040
943
|
from .suspense import start_resource
|
|
1041
944
|
|
|
1042
|
-
ctx =
|
|
1043
|
-
if ctx is None:
|
|
1044
|
-
raise RuntimeError("use_resource must be called inside a @component function")
|
|
945
|
+
ctx = _require_hook_state("use_resource")
|
|
1045
946
|
ctx.record_hook("use_resource")
|
|
1046
947
|
|
|
1047
948
|
idx = ctx.resource_index
|
|
@@ -1082,9 +983,6 @@ def use_transition() -> Tuple[bool, Callable[[Callable[[], None]], None]]:
|
|
|
1082
983
|
|
|
1083
984
|
Example:
|
|
1084
985
|
```python
|
|
1085
|
-
import pythonnative as pn
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
986
|
@pn.component
|
|
1089
987
|
def Search():
|
|
1090
988
|
query, set_query = pn.use_state("")
|
|
@@ -1101,23 +999,24 @@ def use_transition() -> Tuple[bool, Callable[[Callable[[], None]], None]]:
|
|
|
1101
999
|
)
|
|
1102
1000
|
```
|
|
1103
1001
|
"""
|
|
1104
|
-
ctx =
|
|
1105
|
-
if ctx is None:
|
|
1106
|
-
raise RuntimeError("use_transition must be called inside a @component function")
|
|
1002
|
+
ctx = _require_hook_state("use_transition")
|
|
1107
1003
|
|
|
1108
1004
|
is_pending, set_pending = use_state(False)
|
|
1109
1005
|
|
|
1110
1006
|
def start_transition(fn: Callable[[], None]) -> None:
|
|
1007
|
+
owner = ctx.owner
|
|
1008
|
+
if owner is None:
|
|
1009
|
+
fn()
|
|
1010
|
+
return
|
|
1111
1011
|
set_pending(True)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
_schedule_transition_flush()
|
|
1012
|
+
run_in_transition(fn)
|
|
1013
|
+
owner.transitions.on_complete(lambda: set_pending(False))
|
|
1115
1014
|
|
|
1116
1015
|
start = use_callback(start_transition, [])
|
|
1117
1016
|
return is_pending, start
|
|
1118
1017
|
|
|
1119
1018
|
|
|
1120
|
-
def use_deferred_value(value:
|
|
1019
|
+
def use_deferred_value(value: T) -> T:
|
|
1121
1020
|
"""Return a copy of ``value`` that lags behind during fast updates.
|
|
1122
1021
|
|
|
1123
1022
|
The returned value updates in a deferred (transition-priority)
|
|
@@ -1136,14 +1035,12 @@ def use_deferred_value(value: Any) -> Any:
|
|
|
1136
1035
|
Raises:
|
|
1137
1036
|
RuntimeError: If called outside a ``@component`` function.
|
|
1138
1037
|
"""
|
|
1139
|
-
|
|
1140
|
-
if ctx is None:
|
|
1141
|
-
raise RuntimeError("use_deferred_value must be called inside a @component function")
|
|
1038
|
+
_require_hook_state("use_deferred_value")
|
|
1142
1039
|
|
|
1143
1040
|
deferred, set_deferred = use_state(value)
|
|
1144
1041
|
|
|
1145
1042
|
def _adopt() -> None:
|
|
1146
|
-
|
|
1043
|
+
run_in_transition(lambda: set_deferred(value))
|
|
1147
1044
|
|
|
1148
1045
|
use_effect(_adopt, [value])
|
|
1149
1046
|
return deferred
|
|
@@ -1197,14 +1094,11 @@ def use_query(
|
|
|
1197
1094
|
|
|
1198
1095
|
Example:
|
|
1199
1096
|
```python
|
|
1200
|
-
import pythonnative as pn
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
1097
|
@pn.component
|
|
1204
|
-
def UserCard(user_id):
|
|
1098
|
+
def UserCard(user_id: str):
|
|
1205
1099
|
q = pn.use_query(lambda: api.get_user(user_id), [user_id])
|
|
1206
1100
|
if q.loading:
|
|
1207
|
-
return pn.Text("Loading
|
|
1101
|
+
return pn.Text("Loading...")
|
|
1208
1102
|
if q.error:
|
|
1209
1103
|
return pn.Text(f"Error: {q.error}")
|
|
1210
1104
|
return pn.Text(q.data["name"])
|
|
@@ -1231,7 +1125,8 @@ def use_query(
|
|
|
1231
1125
|
except asyncio.CancelledError:
|
|
1232
1126
|
raise
|
|
1233
1127
|
except BaseException as exc: # pragma: no cover - surfaced to user
|
|
1234
|
-
|
|
1128
|
+
failure = exc
|
|
1129
|
+
set_state(lambda s: replace(s, loading=False, error=failure))
|
|
1235
1130
|
|
|
1236
1131
|
future = run_async(_runner())
|
|
1237
1132
|
|
|
@@ -1288,7 +1183,7 @@ class MutationCall(Generic[T]):
|
|
|
1288
1183
|
|
|
1289
1184
|
__slots__ = ("_future",)
|
|
1290
1185
|
|
|
1291
|
-
def __init__(self, future:
|
|
1186
|
+
def __init__(self, future: Any) -> None:
|
|
1292
1187
|
self._future = future
|
|
1293
1188
|
|
|
1294
1189
|
def __await__(self) -> Any:
|
|
@@ -1326,17 +1221,14 @@ def use_mutation(
|
|
|
1326
1221
|
|
|
1327
1222
|
Example:
|
|
1328
1223
|
```python
|
|
1329
|
-
import pythonnative as pn
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
1224
|
@pn.component
|
|
1333
1225
|
def NewPostForm():
|
|
1334
1226
|
state, save = pn.use_mutation(api.create_post)
|
|
1335
1227
|
|
|
1336
1228
|
return pn.Column(
|
|
1337
1229
|
pn.Button("Save", on_press=lambda: save(post)),
|
|
1338
|
-
|
|
1339
|
-
pn.Text(str(state.error))
|
|
1230
|
+
state.loading and pn.Text("Saving..."),
|
|
1231
|
+
state.error and pn.Text(str(state.error)),
|
|
1340
1232
|
)
|
|
1341
1233
|
```
|
|
1342
1234
|
"""
|
|
@@ -1356,7 +1248,8 @@ def use_mutation(
|
|
|
1356
1248
|
set_state(lambda s: replace(s, loading=False))
|
|
1357
1249
|
raise
|
|
1358
1250
|
except BaseException as exc:
|
|
1359
|
-
|
|
1251
|
+
failure = exc
|
|
1252
|
+
set_state(lambda s: replace(s, loading=False, error=failure))
|
|
1360
1253
|
raise
|
|
1361
1254
|
|
|
1362
1255
|
future = run_async(_runner())
|
|
@@ -1366,10 +1259,41 @@ def use_mutation(
|
|
|
1366
1259
|
|
|
1367
1260
|
|
|
1368
1261
|
# ======================================================================
|
|
1369
|
-
#
|
|
1262
|
+
# External subscriptions
|
|
1370
1263
|
# ======================================================================
|
|
1371
1264
|
|
|
1372
1265
|
|
|
1266
|
+
def use_subscription(subscribe: Callable[[Callable[[], None]], Callable[[], None]], get_snapshot: Callable[[], T]) -> T:
|
|
1267
|
+
"""Subscribe to an external store and re-render when its snapshot changes.
|
|
1268
|
+
|
|
1269
|
+
The Pythonic counterpart of React's ``useSyncExternalStore``: the
|
|
1270
|
+
platform-metric hooks below are built on it, and it's the right
|
|
1271
|
+
primitive for app-level stores that live outside the component
|
|
1272
|
+
tree.
|
|
1273
|
+
|
|
1274
|
+
Args:
|
|
1275
|
+
subscribe: ``subscribe(on_change) -> unsubscribe``. Called once
|
|
1276
|
+
on mount; ``on_change`` must be invoked whenever the store
|
|
1277
|
+
changes.
|
|
1278
|
+
get_snapshot: Zero-arg callable returning the current value.
|
|
1279
|
+
Re-read on every render.
|
|
1280
|
+
|
|
1281
|
+
Returns:
|
|
1282
|
+
The current snapshot.
|
|
1283
|
+
|
|
1284
|
+
Raises:
|
|
1285
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1286
|
+
"""
|
|
1287
|
+
_require_hook_state("use_subscription")
|
|
1288
|
+
_, set_tick = use_state(0)
|
|
1289
|
+
|
|
1290
|
+
def _subscribe() -> Callable[[], None]:
|
|
1291
|
+
return subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1292
|
+
|
|
1293
|
+
use_effect(_subscribe, [])
|
|
1294
|
+
return get_snapshot()
|
|
1295
|
+
|
|
1296
|
+
|
|
1373
1297
|
def use_window_dimensions() -> Dict[str, float]:
|
|
1374
1298
|
"""Return the current viewport size and re-render when it changes.
|
|
1375
1299
|
|
|
@@ -1383,32 +1307,11 @@ def use_window_dimensions() -> Dict[str, float]:
|
|
|
1383
1307
|
screen host has run its first layout pass.
|
|
1384
1308
|
|
|
1385
1309
|
Raises:
|
|
1386
|
-
RuntimeError: If called outside a
|
|
1387
|
-
|
|
1388
|
-
Example:
|
|
1389
|
-
```python
|
|
1390
|
-
import pythonnative as pn
|
|
1391
|
-
|
|
1392
|
-
@pn.component
|
|
1393
|
-
def MyView():
|
|
1394
|
-
dims = pn.use_window_dimensions()
|
|
1395
|
-
return pn.Text(f"{dims['width']:.0f} x {dims['height']:.0f}")
|
|
1396
|
-
```
|
|
1310
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1397
1311
|
"""
|
|
1398
1312
|
from . import platform_metrics
|
|
1399
1313
|
|
|
1400
|
-
|
|
1401
|
-
if ctx is None:
|
|
1402
|
-
raise RuntimeError("use_window_dimensions must be called inside a @component function")
|
|
1403
|
-
|
|
1404
|
-
_, set_tick = use_state(0)
|
|
1405
|
-
|
|
1406
|
-
def subscribe() -> Callable[[], None]:
|
|
1407
|
-
return platform_metrics.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1408
|
-
|
|
1409
|
-
use_effect(subscribe, [])
|
|
1410
|
-
|
|
1411
|
-
dims = platform_metrics.get_window_dimensions()
|
|
1314
|
+
dims = use_subscription(platform_metrics.subscribe, platform_metrics.get_window_dimensions)
|
|
1412
1315
|
return {"width": dims.width, "height": dims.height}
|
|
1413
1316
|
|
|
1414
1317
|
|
|
@@ -1422,28 +1325,12 @@ def use_safe_area_insets() -> Dict[str, float]:
|
|
|
1422
1325
|
floats in layout units (pt on iOS, dp on Android).
|
|
1423
1326
|
|
|
1424
1327
|
Raises:
|
|
1425
|
-
RuntimeError: If called outside a
|
|
1328
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1426
1329
|
"""
|
|
1427
1330
|
from . import platform_metrics
|
|
1428
1331
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
raise RuntimeError("use_safe_area_insets must be called inside a @component function")
|
|
1432
|
-
|
|
1433
|
-
_, set_tick = use_state(0)
|
|
1434
|
-
|
|
1435
|
-
def subscribe() -> Callable[[], None]:
|
|
1436
|
-
return platform_metrics.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1437
|
-
|
|
1438
|
-
use_effect(subscribe, [])
|
|
1439
|
-
|
|
1440
|
-
insets = platform_metrics.get_safe_area_insets()
|
|
1441
|
-
return {
|
|
1442
|
-
"top": insets.top,
|
|
1443
|
-
"bottom": insets.bottom,
|
|
1444
|
-
"left": insets.left,
|
|
1445
|
-
"right": insets.right,
|
|
1446
|
-
}
|
|
1332
|
+
insets = use_subscription(platform_metrics.subscribe, platform_metrics.get_safe_area_insets)
|
|
1333
|
+
return {"top": insets.top, "bottom": insets.bottom, "left": insets.left, "right": insets.right}
|
|
1447
1334
|
|
|
1448
1335
|
|
|
1449
1336
|
def use_keyboard_height() -> float:
|
|
@@ -1455,22 +1342,11 @@ def use_keyboard_height() -> float:
|
|
|
1455
1342
|
of reading this directly.
|
|
1456
1343
|
|
|
1457
1344
|
Raises:
|
|
1458
|
-
RuntimeError: If called outside a
|
|
1345
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1459
1346
|
"""
|
|
1460
1347
|
from . import platform_metrics
|
|
1461
1348
|
|
|
1462
|
-
|
|
1463
|
-
if ctx is None:
|
|
1464
|
-
raise RuntimeError("use_keyboard_height must be called inside a @component function")
|
|
1465
|
-
|
|
1466
|
-
_, set_tick = use_state(0)
|
|
1467
|
-
|
|
1468
|
-
def subscribe() -> Callable[[], None]:
|
|
1469
|
-
return platform_metrics.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1470
|
-
|
|
1471
|
-
use_effect(subscribe, [])
|
|
1472
|
-
|
|
1473
|
-
return platform_metrics.get_keyboard_height()
|
|
1349
|
+
return use_subscription(platform_metrics.subscribe, platform_metrics.get_keyboard_height)
|
|
1474
1350
|
|
|
1475
1351
|
|
|
1476
1352
|
def use_color_scheme() -> str:
|
|
@@ -1485,33 +1361,11 @@ def use_color_scheme() -> str:
|
|
|
1485
1361
|
``"light"`` or ``"dark"``.
|
|
1486
1362
|
|
|
1487
1363
|
Raises:
|
|
1488
|
-
RuntimeError: If called outside a
|
|
1489
|
-
|
|
1490
|
-
Example:
|
|
1491
|
-
```python
|
|
1492
|
-
import pythonnative as pn
|
|
1493
|
-
|
|
1494
|
-
@pn.component
|
|
1495
|
-
def Banner():
|
|
1496
|
-
scheme = pn.use_color_scheme()
|
|
1497
|
-
bg = "#000000" if scheme == "dark" else "#FFFFFF"
|
|
1498
|
-
return pn.View(style=pn.style(background_color=bg))
|
|
1499
|
-
```
|
|
1364
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1500
1365
|
"""
|
|
1501
1366
|
from . import appearance
|
|
1502
1367
|
|
|
1503
|
-
|
|
1504
|
-
if ctx is None:
|
|
1505
|
-
raise RuntimeError("use_color_scheme must be called inside a @component function")
|
|
1506
|
-
|
|
1507
|
-
_, set_tick = use_state(0)
|
|
1508
|
-
|
|
1509
|
-
def subscribe() -> Callable[[], None]:
|
|
1510
|
-
return appearance.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1511
|
-
|
|
1512
|
-
use_effect(subscribe, [])
|
|
1513
|
-
|
|
1514
|
-
return appearance.get_color_scheme()
|
|
1368
|
+
return use_subscription(appearance.subscribe, appearance.get_color_scheme)
|
|
1515
1369
|
|
|
1516
1370
|
|
|
1517
1371
|
# ======================================================================
|
|
@@ -1519,249 +1373,121 @@ def use_color_scheme() -> str:
|
|
|
1519
1373
|
# ======================================================================
|
|
1520
1374
|
|
|
1521
1375
|
|
|
1522
|
-
class Context:
|
|
1523
|
-
"""
|
|
1376
|
+
class Context(Generic[T]):
|
|
1377
|
+
"""A value shared with a subtree, created by [`create_context`][pythonnative.create_context].
|
|
1524
1378
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1379
|
+
Provide a value with [`Provider`][pythonnative.hooks.Context.Provider]
|
|
1380
|
+
and read it with [`use_context`][pythonnative.use_context]. A
|
|
1381
|
+
``Context`` is itself an element type: ``ctx.Provider(value, ...)``
|
|
1382
|
+
returns an element whose ``type`` is ``ctx``.
|
|
1528
1383
|
|
|
1529
1384
|
Context is *reactive*: when a Provider's value changes, every
|
|
1530
1385
|
component that read the context on its last render re-renders,
|
|
1531
1386
|
even if a memoized ancestor skipped its own re-render.
|
|
1532
1387
|
|
|
1533
1388
|
Attributes:
|
|
1534
|
-
default: The value returned when no
|
|
1389
|
+
default: The value returned when no Provider ancestor exists.
|
|
1390
|
+
name: Optional label for diagnostics.
|
|
1535
1391
|
"""
|
|
1536
1392
|
|
|
1537
|
-
|
|
1393
|
+
__slots__ = ("default", "name", "_stack")
|
|
1394
|
+
|
|
1395
|
+
def __init__(self, default: T, name: Optional[str] = None) -> None:
|
|
1538
1396
|
self.default = default
|
|
1539
|
-
self.
|
|
1397
|
+
self.name = name
|
|
1398
|
+
self._stack: List[T] = []
|
|
1540
1399
|
|
|
1541
|
-
def
|
|
1400
|
+
def Provider(self, value: T, *children: Node, key: Optional[str] = None) -> Element:
|
|
1401
|
+
"""Provide ``value`` to every descendant of ``children``.
|
|
1402
|
+
|
|
1403
|
+
A Provider contributes no native view of its own; its children
|
|
1404
|
+
mount directly into the surrounding native parent.
|
|
1405
|
+
|
|
1406
|
+
When ``value`` differs from the previous render (identity, then
|
|
1407
|
+
``==``), every descendant that read the context re-renders,
|
|
1408
|
+
including descendants of memoized components that skipped.
|
|
1409
|
+
|
|
1410
|
+
Args:
|
|
1411
|
+
value: Value made available to descendants.
|
|
1412
|
+
*children: Subtree(s) under which the provider applies.
|
|
1413
|
+
key: Stable identity for keyed reconciliation.
|
|
1414
|
+
|
|
1415
|
+
Example:
|
|
1416
|
+
```python
|
|
1417
|
+
Theme = pn.create_context({"primary": "#007AFF"})
|
|
1418
|
+
|
|
1419
|
+
@pn.component
|
|
1420
|
+
def App():
|
|
1421
|
+
return Theme.Provider({"primary": "#FF0000"}, Header(), Body())
|
|
1422
|
+
```
|
|
1423
|
+
"""
|
|
1424
|
+
return Element(self, {"value": value}, children, key=key)
|
|
1425
|
+
|
|
1426
|
+
def current(self) -> T:
|
|
1427
|
+
"""Return the innermost provided value, or ``default``."""
|
|
1542
1428
|
return self._stack[-1] if self._stack else self.default
|
|
1543
1429
|
|
|
1430
|
+
def __repr__(self) -> str:
|
|
1431
|
+
return f"<Context {self.name or id(self):x}>" if self.name is None else f"<Context {self.name}>"
|
|
1432
|
+
|
|
1433
|
+
# Rendering support: the reconciler pushes/pops provided values
|
|
1434
|
+
# while it walks a Provider's subtree.
|
|
1435
|
+
|
|
1436
|
+
def _push(self, value: T) -> None:
|
|
1437
|
+
self._stack.append(value)
|
|
1544
1438
|
|
|
1545
|
-
def
|
|
1439
|
+
def _pop(self) -> None:
|
|
1440
|
+
self._stack.pop()
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
def create_context(default: T = None, *, name: Optional[str] = None) -> Context[T]: # type: ignore[assignment,unused-ignore]
|
|
1546
1444
|
"""Create a new context with an optional default value.
|
|
1547
1445
|
|
|
1548
1446
|
Args:
|
|
1549
1447
|
default: Returned by [`use_context`][pythonnative.use_context]
|
|
1550
|
-
when there is no enclosing
|
|
1551
|
-
|
|
1448
|
+
when there is no enclosing Provider.
|
|
1449
|
+
name: Optional label shown in diagnostics.
|
|
1552
1450
|
|
|
1553
1451
|
Returns:
|
|
1554
|
-
A fresh `Context`
|
|
1452
|
+
A fresh [`Context`][pythonnative.Context].
|
|
1555
1453
|
|
|
1556
1454
|
Example:
|
|
1557
1455
|
```python
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
ThemeContext = pn.create_context({"primary": "#007AFF"})
|
|
1456
|
+
Theme = pn.create_context({"primary": "#007AFF"}, name="Theme")
|
|
1561
1457
|
```
|
|
1562
1458
|
"""
|
|
1563
|
-
return Context(default)
|
|
1459
|
+
return Context(default, name=name)
|
|
1564
1460
|
|
|
1565
1461
|
|
|
1566
|
-
def use_context(context: Context) ->
|
|
1567
|
-
"""Read the current value of
|
|
1462
|
+
def use_context(context: Context[T]) -> T:
|
|
1463
|
+
"""Read the current value of ``context`` from the nearest Provider.
|
|
1568
1464
|
|
|
1569
|
-
If no enclosing
|
|
1465
|
+
If no enclosing Provider exists, returns the context's default.
|
|
1570
1466
|
The component is registered as a subscriber: when the nearest
|
|
1571
1467
|
Provider's value changes, the component re-renders even if a
|
|
1572
1468
|
memoized ancestor skipped.
|
|
1573
1469
|
|
|
1574
1470
|
Args:
|
|
1575
|
-
context: The `Context` to read from.
|
|
1471
|
+
context: The [`Context`][pythonnative.Context] to read from.
|
|
1576
1472
|
|
|
1577
1473
|
Returns:
|
|
1578
|
-
The current value for
|
|
1474
|
+
The current value for ``context``.
|
|
1579
1475
|
|
|
1580
1476
|
Raises:
|
|
1581
|
-
RuntimeError: If called outside a
|
|
1477
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1582
1478
|
"""
|
|
1583
|
-
ctx =
|
|
1584
|
-
if ctx is None:
|
|
1585
|
-
raise RuntimeError("use_context must be called inside a @component function")
|
|
1479
|
+
ctx = _require_hook_state("use_context")
|
|
1586
1480
|
ctx.record_hook("use_context")
|
|
1587
|
-
value = context.
|
|
1481
|
+
value = context.current()
|
|
1588
1482
|
ctx.context_deps[id(context)] = value
|
|
1589
1483
|
return value
|
|
1590
1484
|
|
|
1591
1485
|
|
|
1592
1486
|
# ======================================================================
|
|
1593
|
-
#
|
|
1487
|
+
# System back button
|
|
1594
1488
|
# ======================================================================
|
|
1595
1489
|
|
|
1596
1490
|
|
|
1597
|
-
def Provider(context: "Context", value: Any, *children: Element) -> Element:
|
|
1598
|
-
"""Provide ``value`` for ``context`` to all descendants of ``children``.
|
|
1599
|
-
|
|
1600
|
-
Accepts any number of children (varargs). A Provider contributes no
|
|
1601
|
-
native view of its own; its children mount directly into the
|
|
1602
|
-
surrounding native parent.
|
|
1603
|
-
|
|
1604
|
-
When ``value`` differs from the previous render (identity, then
|
|
1605
|
-
``==``), every descendant that read the context via
|
|
1606
|
-
[`use_context`][pythonnative.use_context] re-renders, including
|
|
1607
|
-
descendants of memoized components that skipped.
|
|
1608
|
-
|
|
1609
|
-
Args:
|
|
1610
|
-
context: The [`Context`][pythonnative.hooks.Context] to set.
|
|
1611
|
-
value: Value made available to descendants via
|
|
1612
|
-
[`use_context`][pythonnative.use_context].
|
|
1613
|
-
*children: Subtree(s) under which the provider applies.
|
|
1614
|
-
|
|
1615
|
-
Returns:
|
|
1616
|
-
An [`Element`][pythonnative.Element] that the reconciler treats
|
|
1617
|
-
as a context boundary.
|
|
1618
|
-
|
|
1619
|
-
Example:
|
|
1620
|
-
```python
|
|
1621
|
-
import pythonnative as pn
|
|
1622
|
-
|
|
1623
|
-
ThemeContext = pn.create_context({"primary": "#007AFF"})
|
|
1624
|
-
|
|
1625
|
-
@pn.component
|
|
1626
|
-
def App():
|
|
1627
|
-
return pn.Provider(
|
|
1628
|
-
ThemeContext,
|
|
1629
|
-
{"primary": "#FF0000"},
|
|
1630
|
-
Header(),
|
|
1631
|
-
Body(),
|
|
1632
|
-
)
|
|
1633
|
-
```
|
|
1634
|
-
"""
|
|
1635
|
-
return Element("__Provider__", {"__context__": context, "__value__": value}, list(children))
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
def memo(component_fn: Callable[..., Element]) -> Callable[..., Element]:
|
|
1639
|
-
"""Skip a function component's render when its props haven't changed.
|
|
1640
|
-
|
|
1641
|
-
Decorate a ``@component``-wrapped function to opt into shallow-prop
|
|
1642
|
-
memoization. When the reconciler re-renders the parent tree, a
|
|
1643
|
-
memoized child is skipped (its previously-rendered subtree is
|
|
1644
|
-
reused) iff:
|
|
1645
|
-
|
|
1646
|
-
- Its props are shallowly equal to the previous render's props
|
|
1647
|
-
(callables compared by identity, scalars by ``==``).
|
|
1648
|
-
- None of its internal ``use_state`` / ``use_reducer`` setters fired
|
|
1649
|
-
since the last render.
|
|
1650
|
-
|
|
1651
|
-
Pair with [`use_callback`][pythonnative.use_callback] when passing
|
|
1652
|
-
callbacks as props, otherwise a fresh closure will defeat the memo.
|
|
1653
|
-
|
|
1654
|
-
Args:
|
|
1655
|
-
component_fn: A function previously decorated with
|
|
1656
|
-
[`component`][pythonnative.component].
|
|
1657
|
-
|
|
1658
|
-
Returns:
|
|
1659
|
-
The same function, marked for memoization.
|
|
1660
|
-
|
|
1661
|
-
Example:
|
|
1662
|
-
```python
|
|
1663
|
-
import pythonnative as pn
|
|
1664
|
-
|
|
1665
|
-
@pn.memo
|
|
1666
|
-
@pn.component
|
|
1667
|
-
def ExpensiveRow(label: str):
|
|
1668
|
-
...
|
|
1669
|
-
```
|
|
1670
|
-
"""
|
|
1671
|
-
component_fn._pn_memo = True
|
|
1672
|
-
# ``@component`` builds a wrapper that emits an ``Element`` whose
|
|
1673
|
-
# ``type`` is the underlying function, so propagate the marker to
|
|
1674
|
-
# ``__wrapped__`` so the reconciler can find it via ``Element.type``.
|
|
1675
|
-
wrapped = getattr(component_fn, "__wrapped__", None)
|
|
1676
|
-
if wrapped is not None:
|
|
1677
|
-
wrapped._pn_memo = True
|
|
1678
|
-
return component_fn
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
# ======================================================================
|
|
1682
|
-
# Navigation
|
|
1683
|
-
# ======================================================================
|
|
1684
|
-
|
|
1685
|
-
_NavigationContext: Context = create_context(None)
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
class NavigationHandle:
|
|
1689
|
-
"""Handle returned by [`use_navigation`][pythonnative.use_navigation].
|
|
1690
|
-
|
|
1691
|
-
Wraps the host's push/pop primitives so screens can navigate
|
|
1692
|
-
without knowing the underlying native navigation stack. The
|
|
1693
|
-
typical user-facing surface is the declarative handle returned by
|
|
1694
|
-
a [`Stack`][pythonnative.create_stack_navigator]; this class is
|
|
1695
|
-
the lower-level fallback used when no navigator is rendered (and
|
|
1696
|
-
as the bridge that declarative navigators delegate to when they
|
|
1697
|
-
need to push real native screens).
|
|
1698
|
-
|
|
1699
|
-
Example:
|
|
1700
|
-
```python
|
|
1701
|
-
import pythonnative as pn
|
|
1702
|
-
|
|
1703
|
-
@pn.component
|
|
1704
|
-
def HomeScreen():
|
|
1705
|
-
nav = pn.use_navigation()
|
|
1706
|
-
return pn.Button(
|
|
1707
|
-
"Open Detail",
|
|
1708
|
-
on_press=lambda: nav.navigate("Detail", {"id": 42}),
|
|
1709
|
-
)
|
|
1710
|
-
```
|
|
1711
|
-
"""
|
|
1712
|
-
|
|
1713
|
-
def __init__(self, host: Any) -> None:
|
|
1714
|
-
self._host = host
|
|
1715
|
-
|
|
1716
|
-
def navigate(self, component: Any, params: Optional[Dict[str, Any]] = None) -> None:
|
|
1717
|
-
"""Push ``component`` onto the navigation stack.
|
|
1718
|
-
|
|
1719
|
-
Args:
|
|
1720
|
-
component: A ``@component`` function or a dotted Python
|
|
1721
|
-
path (e.g. ``"app.detail.DetailScreen"``). When a
|
|
1722
|
-
Stack navigator is the root of the app, prefer the
|
|
1723
|
-
declarative ``nav.navigate("Detail", params)`` form
|
|
1724
|
-
returned by ``use_navigation()`` (it pushes by route
|
|
1725
|
-
name and the host re-uses its own ``App`` component).
|
|
1726
|
-
params: Optional dict of arguments serialized into the
|
|
1727
|
-
target screen.
|
|
1728
|
-
"""
|
|
1729
|
-
self._host._push(component, params)
|
|
1730
|
-
|
|
1731
|
-
def go_back(self) -> None:
|
|
1732
|
-
"""Pop the current screen and return to the previous one."""
|
|
1733
|
-
self._host._pop()
|
|
1734
|
-
|
|
1735
|
-
def get_params(self) -> Dict[str, Any]:
|
|
1736
|
-
"""Return the params dict passed to this screen.
|
|
1737
|
-
|
|
1738
|
-
Returns:
|
|
1739
|
-
The dict supplied by the caller's
|
|
1740
|
-
[`navigate`][pythonnative.hooks.NavigationHandle.navigate]
|
|
1741
|
-
call, or an empty dict if none was supplied.
|
|
1742
|
-
"""
|
|
1743
|
-
return self._host._get_nav_args()
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
def use_navigation() -> NavigationHandle:
|
|
1747
|
-
"""Return a [`NavigationHandle`][pythonnative.hooks.NavigationHandle] for the screen.
|
|
1748
|
-
|
|
1749
|
-
Returns:
|
|
1750
|
-
The handle bound to the current screen's host.
|
|
1751
|
-
|
|
1752
|
-
Raises:
|
|
1753
|
-
RuntimeError: If called outside a component rendered via
|
|
1754
|
-
[`create_screen`][pythonnative.create_screen].
|
|
1755
|
-
"""
|
|
1756
|
-
handle = use_context(_NavigationContext)
|
|
1757
|
-
if handle is None:
|
|
1758
|
-
raise RuntimeError(
|
|
1759
|
-
"use_navigation() called outside a PythonNative screen. "
|
|
1760
|
-
"Ensure your component is rendered via create_screen()."
|
|
1761
|
-
)
|
|
1762
|
-
return handle
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
1491
|
def use_back_handler(handler: Callable[[], bool]) -> None:
|
|
1766
1492
|
"""Intercept the system back action for this screen.
|
|
1767
1493
|
|
|
@@ -1786,12 +1512,10 @@ def use_back_handler(handler: Callable[[], bool]) -> None:
|
|
|
1786
1512
|
the back action.
|
|
1787
1513
|
|
|
1788
1514
|
Raises:
|
|
1789
|
-
RuntimeError: If called outside a
|
|
1515
|
+
RuntimeError: If called outside a ``@component`` function.
|
|
1790
1516
|
|
|
1791
1517
|
Example:
|
|
1792
1518
|
```python
|
|
1793
|
-
import pythonnative as pn
|
|
1794
|
-
|
|
1795
1519
|
@pn.component
|
|
1796
1520
|
def Editor():
|
|
1797
1521
|
dirty, set_dirty = pn.use_state(False)
|
|
@@ -1799,16 +1523,14 @@ def use_back_handler(handler: Callable[[], bool]) -> None:
|
|
|
1799
1523
|
...
|
|
1800
1524
|
```
|
|
1801
1525
|
"""
|
|
1802
|
-
ctx =
|
|
1803
|
-
if ctx is None:
|
|
1804
|
-
raise RuntimeError("use_back_handler must be called inside a @component function")
|
|
1526
|
+
ctx = _require_hook_state("use_back_handler")
|
|
1805
1527
|
|
|
1806
1528
|
latest: Ref[Callable[[], bool]] = use_ref(handler)
|
|
1807
1529
|
latest.current = handler
|
|
1808
1530
|
|
|
1809
1531
|
def _register() -> Optional[Callable[[], None]]:
|
|
1810
|
-
|
|
1811
|
-
if
|
|
1532
|
+
owner = ctx.owner
|
|
1533
|
+
if owner is None:
|
|
1812
1534
|
return None
|
|
1813
1535
|
|
|
1814
1536
|
def _trampoline() -> bool:
|
|
@@ -1822,68 +1544,41 @@ def use_back_handler(handler: Callable[[], bool]) -> None:
|
|
|
1822
1544
|
raise
|
|
1823
1545
|
return True
|
|
1824
1546
|
|
|
1825
|
-
return
|
|
1547
|
+
return owner.register_back_handler(_trampoline)
|
|
1826
1548
|
|
|
1827
1549
|
use_effect(_register, [])
|
|
1828
1550
|
|
|
1829
1551
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
""
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
sig = inspect.signature(func)
|
|
1864
|
-
positional_params = [
|
|
1865
|
-
name
|
|
1866
|
-
for name, p in sig.parameters.items()
|
|
1867
|
-
if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
1868
|
-
]
|
|
1869
|
-
has_var_positional = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values())
|
|
1870
|
-
|
|
1871
|
-
def wrapper(*args: Any, **kwargs: Any) -> Element:
|
|
1872
|
-
props: dict = dict(kwargs)
|
|
1873
|
-
|
|
1874
|
-
if args:
|
|
1875
|
-
if has_var_positional:
|
|
1876
|
-
props["children"] = list(args)
|
|
1877
|
-
else:
|
|
1878
|
-
for i, arg in enumerate(args):
|
|
1879
|
-
if i < len(positional_params):
|
|
1880
|
-
props[positional_params[i]] = arg
|
|
1881
|
-
|
|
1882
|
-
key = props.pop("key", None)
|
|
1883
|
-
return Element(func, props, [], key=key)
|
|
1884
|
-
|
|
1885
|
-
wrapper.__wrapped__ = func # noqa: B010
|
|
1886
|
-
wrapper.__name__ = func.__name__
|
|
1887
|
-
wrapper.__qualname__ = func.__qualname__
|
|
1888
|
-
wrapper._pn_component = True # noqa: B010
|
|
1889
|
-
return wrapper
|
|
1552
|
+
__all__ = [
|
|
1553
|
+
"Context",
|
|
1554
|
+
"HookState",
|
|
1555
|
+
"MutationCall",
|
|
1556
|
+
"MutationState",
|
|
1557
|
+
"QueryResult",
|
|
1558
|
+
"Ref",
|
|
1559
|
+
"RenderOwner",
|
|
1560
|
+
"create_context",
|
|
1561
|
+
"current_hook_state",
|
|
1562
|
+
"install_hook_state",
|
|
1563
|
+
"restore_hook_state",
|
|
1564
|
+
"use_back_handler",
|
|
1565
|
+
"use_callback",
|
|
1566
|
+
"use_color_scheme",
|
|
1567
|
+
"use_context",
|
|
1568
|
+
"use_deferred_value",
|
|
1569
|
+
"use_effect",
|
|
1570
|
+
"use_imperative_handle",
|
|
1571
|
+
"use_keyboard_height",
|
|
1572
|
+
"use_layout_effect",
|
|
1573
|
+
"use_memo",
|
|
1574
|
+
"use_mutation",
|
|
1575
|
+
"use_query",
|
|
1576
|
+
"use_reducer",
|
|
1577
|
+
"use_ref",
|
|
1578
|
+
"use_resource",
|
|
1579
|
+
"use_safe_area_insets",
|
|
1580
|
+
"use_state",
|
|
1581
|
+
"use_subscription",
|
|
1582
|
+
"use_transition",
|
|
1583
|
+
"use_window_dimensions",
|
|
1584
|
+
]
|