pythonnative 0.22.1__py3-none-any.whl → 0.24.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 +43 -50
- pythonnative/animated.py +1 -1
- pythonnative/appearance.py +124 -0
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +873 -466
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +454 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/images.py +196 -0
- pythonnative/mutations.py +3 -12
- pythonnative/native_views/__init__.py +1 -7
- pythonnative/native_views/android.py +651 -46
- pythonnative/native_views/desktop.py +116 -20
- pythonnative/native_views/ios.py +974 -52
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +74 -7
- pythonnative/project/config.py +1 -9
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +409 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +148 -6
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- pythonnative/virtual_rows.py +137 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/hooks.py
CHANGED
|
@@ -4,12 +4,18 @@ Provides React-like hooks for managing state, effects, memoization,
|
|
|
4
4
|
context, and navigation within function components decorated with
|
|
5
5
|
[`component`][pythonnative.component]. Hooks must be called at the top
|
|
6
6
|
level of a component (not inside conditionals or loops) so they map to
|
|
7
|
-
the same slot across renders.
|
|
7
|
+
the same slot across renders. In dev mode the framework verifies this
|
|
8
|
+
and raises [`HookOrderError`][pythonnative.diagnostics.HookOrderError]
|
|
9
|
+
on a violation instead of silently cross-wiring state.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Two effect phases exist, mirroring React:
|
|
12
|
+
|
|
13
|
+
- [`use_layout_effect`][pythonnative.use_layout_effect] callbacks run
|
|
14
|
+
synchronously inside the commit, after native mutations and the
|
|
15
|
+
layout pass have been applied. They can measure committed frames and
|
|
16
|
+
issue imperative view commands before the user sees the new frame.
|
|
17
|
+
- [`use_effect`][pythonnative.use_effect] callbacks (passive effects)
|
|
18
|
+
run after the layout effects, at the end of the same commit.
|
|
13
19
|
|
|
14
20
|
Example:
|
|
15
21
|
```python
|
|
@@ -20,7 +26,7 @@ Example:
|
|
|
20
26
|
count, set_count = pn.use_state(initial)
|
|
21
27
|
return pn.Column(
|
|
22
28
|
pn.Text(f"Count: {count}"),
|
|
23
|
-
pn.Button("+",
|
|
29
|
+
pn.Button("+", on_press=lambda: set_count(count + 1)),
|
|
24
30
|
)
|
|
25
31
|
```
|
|
26
32
|
"""
|
|
@@ -32,6 +38,7 @@ from contextlib import contextmanager
|
|
|
32
38
|
from dataclasses import dataclass, field, replace
|
|
33
39
|
from typing import Any, Awaitable, Callable, Dict, Generator, Generic, List, Optional, Tuple, TypeVar
|
|
34
40
|
|
|
41
|
+
from . import diagnostics
|
|
35
42
|
from .element import Element
|
|
36
43
|
|
|
37
44
|
T = TypeVar("T")
|
|
@@ -42,6 +49,48 @@ _hook_context: threading.local = threading.local()
|
|
|
42
49
|
|
|
43
50
|
_batch_context: threading.local = threading.local()
|
|
44
51
|
|
|
52
|
+
|
|
53
|
+
# ======================================================================
|
|
54
|
+
# Ref
|
|
55
|
+
# ======================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Ref(Generic[T]):
|
|
59
|
+
"""Mutable container returned by [`use_ref`][pythonnative.use_ref].
|
|
60
|
+
|
|
61
|
+
A ``Ref`` holds one value on its ``current`` attribute. Mutating
|
|
62
|
+
``current`` never triggers a re-render, which makes refs the right
|
|
63
|
+
place for timers, last-seen values, and imperative handles.
|
|
64
|
+
|
|
65
|
+
When a ``Ref`` is passed to a built-in element via the ``ref=``
|
|
66
|
+
prop, the reconciler populates ``current`` with the underlying
|
|
67
|
+
native view (``UIView`` on iOS, ``android.view.View`` on Android,
|
|
68
|
+
a Tk widget on desktop) after commit, and clears it back to
|
|
69
|
+
``None`` on unmount. Composite components (e.g.
|
|
70
|
+
[`FlatList`][pythonnative.FlatList]) instead publish a typed
|
|
71
|
+
controller object on ``current`` via
|
|
72
|
+
[`use_imperative_handle`][pythonnative.use_imperative_handle].
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
current: The referenced value. ``None`` until populated.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
__slots__ = ("current", "_pn_tag", "_pn_frame")
|
|
79
|
+
|
|
80
|
+
def __init__(self, initial: Optional[T] = None) -> None:
|
|
81
|
+
self.current: Optional[T] = initial
|
|
82
|
+
# Internal: the native view tag, populated by the reconciler
|
|
83
|
+
# when the ref is attached to a built-in element.
|
|
84
|
+
self._pn_tag: Optional[int] = None
|
|
85
|
+
# Internal: the last committed frame ``(x, y, w, h)``, mirrored
|
|
86
|
+
# by the layout pass so Python code can read measured geometry
|
|
87
|
+
# without a native round-trip.
|
|
88
|
+
self._pn_frame: Optional[Tuple[float, float, float, float]] = None
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return f"Ref({self.current!r})"
|
|
92
|
+
|
|
93
|
+
|
|
45
94
|
# ======================================================================
|
|
46
95
|
# Hook state container
|
|
47
96
|
# ======================================================================
|
|
@@ -53,44 +102,61 @@ class HookState:
|
|
|
53
102
|
Each `@component` instance owns one `HookState`. Hooks are matched
|
|
54
103
|
to slots by call order, so they must always be called in the same
|
|
55
104
|
order across renders. Effects scheduled during render are deferred
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
(layout effects into ``_pending_layout_effects``, passive effects
|
|
106
|
+
into ``_pending_effects``) and flushed by the reconciler in two
|
|
107
|
+
phases after native mutations commit.
|
|
59
108
|
|
|
60
109
|
Attributes:
|
|
61
110
|
states: One entry per `use_state` / `use_reducer` call.
|
|
62
111
|
effects: One `(deps, cleanup)` tuple per `use_effect` call.
|
|
112
|
+
layout_effects: One `(deps, cleanup)` tuple per
|
|
113
|
+
`use_layout_effect` call.
|
|
63
114
|
memos: One `(deps, value)` tuple per `use_memo` / `use_callback`.
|
|
64
|
-
refs: One
|
|
115
|
+
refs: One [`Ref`][pythonnative.Ref] per `use_ref` call.
|
|
65
116
|
"""
|
|
66
117
|
|
|
67
118
|
__slots__ = (
|
|
68
119
|
"states",
|
|
69
120
|
"effects",
|
|
121
|
+
"layout_effects",
|
|
70
122
|
"memos",
|
|
71
123
|
"refs",
|
|
72
124
|
"state_index",
|
|
73
125
|
"effect_index",
|
|
126
|
+
"layout_effect_index",
|
|
74
127
|
"memo_index",
|
|
75
128
|
"ref_index",
|
|
129
|
+
"context_deps",
|
|
76
130
|
"_trigger_render",
|
|
77
131
|
"_pending_effects",
|
|
132
|
+
"_pending_layout_effects",
|
|
78
133
|
"_dirty",
|
|
79
134
|
"_vnode",
|
|
80
135
|
"_reconciler",
|
|
136
|
+
"_hook_log",
|
|
137
|
+
"_hook_signature",
|
|
138
|
+
"_component_name",
|
|
81
139
|
)
|
|
82
140
|
|
|
83
141
|
def __init__(self) -> None:
|
|
84
142
|
self.states: List[Any] = []
|
|
85
143
|
self.effects: List[Tuple[Any, Any]] = []
|
|
144
|
+
self.layout_effects: List[Tuple[Any, Any]] = []
|
|
86
145
|
self.memos: List[Tuple[Any, Any]] = []
|
|
87
|
-
self.refs: List[
|
|
146
|
+
self.refs: List[Ref] = []
|
|
88
147
|
self.state_index: int = 0
|
|
89
148
|
self.effect_index: int = 0
|
|
149
|
+
self.layout_effect_index: int = 0
|
|
90
150
|
self.memo_index: int = 0
|
|
91
151
|
self.ref_index: int = 0
|
|
152
|
+
# Contexts read during the last completed render, keyed by
|
|
153
|
+
# ``id(context)``. The reconciler consults this when a
|
|
154
|
+
# Provider's value changes so consumers re-render even when a
|
|
155
|
+
# memoized ancestor skipped (reactive context).
|
|
156
|
+
self.context_deps: Dict[int, Any] = {}
|
|
92
157
|
self._trigger_render: Optional[Callable[[], None]] = None
|
|
93
158
|
self._pending_effects: List[Tuple[int, Callable, Any]] = []
|
|
159
|
+
self._pending_layout_effects: List[Tuple[int, Callable, Any]] = []
|
|
94
160
|
# Cleared by the reconciler after each successful render.
|
|
95
161
|
# ``use_state`` / ``use_reducer`` setters flip it to ``True``
|
|
96
162
|
# whenever they actually mutate state, so [`memo`][pythonnative.memo]
|
|
@@ -104,21 +170,108 @@ class HookState:
|
|
|
104
170
|
# again when it unmounts.
|
|
105
171
|
self._vnode: Any = None
|
|
106
172
|
self._reconciler: Any = None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
173
|
+
# Dev-mode hook-order guard: the sequence of hook kinds called
|
|
174
|
+
# during the in-flight render, and the signature captured from
|
|
175
|
+
# the first successful render.
|
|
176
|
+
self._hook_log: Optional[List[str]] = None
|
|
177
|
+
self._hook_signature: Optional[List[str]] = None
|
|
178
|
+
self._component_name: str = ""
|
|
179
|
+
|
|
180
|
+
def begin_render(self, component_name: str = "") -> None:
|
|
181
|
+
"""Prepare for a render pass: reset cursors and the dev-mode hook log.
|
|
182
|
+
|
|
183
|
+
Called by the reconciler before each invocation of the
|
|
184
|
+
component body so the next render reads slots in the same
|
|
185
|
+
order they were written.
|
|
114
186
|
"""
|
|
115
187
|
self.state_index = 0
|
|
116
188
|
self.effect_index = 0
|
|
189
|
+
self.layout_effect_index = 0
|
|
117
190
|
self.memo_index = 0
|
|
118
191
|
self.ref_index = 0
|
|
192
|
+
self.context_deps = {}
|
|
193
|
+
if component_name:
|
|
194
|
+
self._component_name = component_name
|
|
195
|
+
self._hook_log = [] if diagnostics.is_dev() else None
|
|
196
|
+
|
|
197
|
+
def finish_render(self) -> None:
|
|
198
|
+
"""Finalize a successful render: lock in / verify the hook signature.
|
|
199
|
+
|
|
200
|
+
Only called when the component body returned without raising,
|
|
201
|
+
so a failed render never corrupts the signature.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
HookOrderError: In dev mode, when this render called fewer
|
|
205
|
+
hooks than the previous one.
|
|
206
|
+
"""
|
|
207
|
+
log = self._hook_log
|
|
208
|
+
self._hook_log = None
|
|
209
|
+
if log is None:
|
|
210
|
+
return
|
|
211
|
+
if self._hook_signature is None:
|
|
212
|
+
self._hook_signature = log
|
|
213
|
+
return
|
|
214
|
+
if len(log) < len(self._hook_signature):
|
|
215
|
+
missing = self._hook_signature[len(log)]
|
|
216
|
+
raise diagnostics.HookOrderError(
|
|
217
|
+
f"{self._component_name or 'Component'} rendered fewer hooks than the previous "
|
|
218
|
+
f"render (expected {missing!r} at position {len(log) + 1}). Hooks must be called "
|
|
219
|
+
"unconditionally, in the same order, on every render."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def record_hook(self, kind: str) -> None:
|
|
223
|
+
"""Record a hook call for the dev-mode order guard.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
HookOrderError: In dev mode, when the hook at this position
|
|
227
|
+
differs from (or extends past) the previous render.
|
|
228
|
+
"""
|
|
229
|
+
log = self._hook_log
|
|
230
|
+
if log is None:
|
|
231
|
+
return
|
|
232
|
+
position = len(log)
|
|
233
|
+
log.append(kind)
|
|
234
|
+
signature = self._hook_signature
|
|
235
|
+
if signature is None:
|
|
236
|
+
return
|
|
237
|
+
if position >= len(signature):
|
|
238
|
+
raise diagnostics.HookOrderError(
|
|
239
|
+
f"{self._component_name or 'Component'} rendered more hooks than the previous "
|
|
240
|
+
f"render ({kind!r} at position {position + 1}). Hooks must be called "
|
|
241
|
+
"unconditionally, in the same order, on every render."
|
|
242
|
+
)
|
|
243
|
+
if signature[position] != kind:
|
|
244
|
+
raise diagnostics.HookOrderError(
|
|
245
|
+
f"{self._component_name or 'Component'} called {kind!r} at position "
|
|
246
|
+
f"{position + 1}, but the previous render called {signature[position]!r} there. "
|
|
247
|
+
"Hooks must be called unconditionally, in the same order, on every render."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def reset_hook_signature(self) -> None:
|
|
251
|
+
"""Forget the recorded hook signature (used by Fast Refresh).
|
|
252
|
+
|
|
253
|
+
After a hot reload swaps in a new component body, the old
|
|
254
|
+
signature no longer applies; the next render records a fresh
|
|
255
|
+
one.
|
|
256
|
+
"""
|
|
257
|
+
self._hook_signature = None
|
|
258
|
+
|
|
259
|
+
def flush_layout_effects(self) -> None:
|
|
260
|
+
"""Run layout effects queued during render (commit phase, pre-paint)."""
|
|
261
|
+
pending = self._pending_layout_effects
|
|
262
|
+
self._pending_layout_effects = []
|
|
263
|
+
for idx, effect_fn, deps in pending:
|
|
264
|
+
_, prev_cleanup = self.layout_effects[idx]
|
|
265
|
+
if callable(prev_cleanup):
|
|
266
|
+
try:
|
|
267
|
+
prev_cleanup()
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
cleanup = effect_fn()
|
|
271
|
+
self.layout_effects[idx] = (list(deps) if deps is not None else None, cleanup)
|
|
119
272
|
|
|
120
273
|
def flush_pending_effects(self) -> None:
|
|
121
|
-
"""Run effects queued during render, after native commit.
|
|
274
|
+
"""Run passive effects queued during render, after native commit.
|
|
122
275
|
|
|
123
276
|
For each pending effect, the previous cleanup is invoked first
|
|
124
277
|
(if any), then the new effect callback. The new return value
|
|
@@ -139,10 +292,18 @@ class HookState:
|
|
|
139
292
|
def cleanup_all_effects(self) -> None:
|
|
140
293
|
"""Run every outstanding cleanup function, then clear state.
|
|
141
294
|
|
|
142
|
-
|
|
143
|
-
|
|
295
|
+
Layout-effect cleanups run before passive-effect cleanups,
|
|
296
|
+
matching the mount order in reverse. Called when the component
|
|
297
|
+
instance is unmounted by the reconciler.
|
|
144
298
|
"""
|
|
145
|
-
for i, (
|
|
299
|
+
for i, (_deps, cleanup) in enumerate(self.layout_effects):
|
|
300
|
+
if callable(cleanup):
|
|
301
|
+
try:
|
|
302
|
+
cleanup()
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
self.layout_effects[i] = (_SENTINEL, None)
|
|
306
|
+
for i, (_deps, cleanup) in enumerate(self.effects):
|
|
146
307
|
if callable(cleanup):
|
|
147
308
|
try:
|
|
148
309
|
cleanup()
|
|
@@ -150,6 +311,7 @@ class HookState:
|
|
|
150
311
|
pass
|
|
151
312
|
self.effects[i] = (_SENTINEL, None)
|
|
152
313
|
self._pending_effects = []
|
|
314
|
+
self._pending_layout_effects = []
|
|
153
315
|
|
|
154
316
|
|
|
155
317
|
# ======================================================================
|
|
@@ -278,13 +440,14 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
|
|
|
278
440
|
count, set_count = pn.use_state(0)
|
|
279
441
|
return pn.Button(
|
|
280
442
|
f"Count: {count}",
|
|
281
|
-
|
|
443
|
+
on_press=lambda: set_count(count + 1),
|
|
282
444
|
)
|
|
283
445
|
```
|
|
284
446
|
"""
|
|
285
447
|
ctx = _get_hook_state()
|
|
286
448
|
if ctx is None:
|
|
287
449
|
raise RuntimeError("use_state must be called inside a @component function")
|
|
450
|
+
ctx.record_hook("use_state")
|
|
288
451
|
|
|
289
452
|
idx = ctx.state_index
|
|
290
453
|
ctx.state_index += 1
|
|
@@ -343,14 +506,15 @@ def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple
|
|
|
343
506
|
def Counter():
|
|
344
507
|
count, dispatch = pn.use_reducer(reducer, 0)
|
|
345
508
|
return pn.Row(
|
|
346
|
-
pn.Button("+",
|
|
347
|
-
pn.Button("Reset",
|
|
509
|
+
pn.Button("+", on_press=lambda: dispatch("increment")),
|
|
510
|
+
pn.Button("Reset", on_press=lambda: dispatch("reset")),
|
|
348
511
|
)
|
|
349
512
|
```
|
|
350
513
|
"""
|
|
351
514
|
ctx = _get_hook_state()
|
|
352
515
|
if ctx is None:
|
|
353
516
|
raise RuntimeError("use_reducer must be called inside a @component function")
|
|
517
|
+
ctx.record_hook("use_reducer")
|
|
354
518
|
|
|
355
519
|
idx = ctx.state_index
|
|
356
520
|
ctx.state_index += 1
|
|
@@ -416,6 +580,7 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
416
580
|
ctx = _get_hook_state()
|
|
417
581
|
if ctx is None:
|
|
418
582
|
raise RuntimeError("use_effect must be called inside a @component function")
|
|
583
|
+
ctx.record_hook("use_effect")
|
|
419
584
|
|
|
420
585
|
idx = ctx.effect_index
|
|
421
586
|
ctx.effect_index += 1
|
|
@@ -430,6 +595,61 @@ def use_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
|
430
595
|
ctx._pending_effects.append((idx, effect, deps))
|
|
431
596
|
|
|
432
597
|
|
|
598
|
+
def use_layout_effect(effect: Callable, deps: Optional[list] = None) -> None:
|
|
599
|
+
"""Schedule a side effect that runs synchronously inside the commit.
|
|
600
|
+
|
|
601
|
+
Like [`use_effect`][pythonnative.use_effect], but the callback
|
|
602
|
+
fires *before* passive effects, immediately after native mutations
|
|
603
|
+
and the layout pass are applied. Use it when you need to measure a
|
|
604
|
+
committed frame (via a [`Ref`][pythonnative.Ref]) or issue an
|
|
605
|
+
imperative view command before the user sees the new frame, for
|
|
606
|
+
example scrolling a list into position on mount.
|
|
607
|
+
|
|
608
|
+
Prefer `use_effect` for everything else; layout effects block the
|
|
609
|
+
commit, so heavy work here delays the frame.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
effect: A zero-arg callable invoked during commit. Optionally
|
|
613
|
+
returns a cleanup callable.
|
|
614
|
+
deps: Dependency list, or `None` to run on every render.
|
|
615
|
+
|
|
616
|
+
Raises:
|
|
617
|
+
RuntimeError: If called outside a `@component` function.
|
|
618
|
+
|
|
619
|
+
Example:
|
|
620
|
+
```python
|
|
621
|
+
import pythonnative as pn
|
|
622
|
+
|
|
623
|
+
@pn.component
|
|
624
|
+
def AutoScrollList(items):
|
|
625
|
+
list_ref = pn.use_ref()
|
|
626
|
+
|
|
627
|
+
def scroll_to_bottom():
|
|
628
|
+
if list_ref.current is not None:
|
|
629
|
+
list_ref.current.scroll_to_end(animated=False)
|
|
630
|
+
|
|
631
|
+
pn.use_layout_effect(scroll_to_bottom, [len(items)])
|
|
632
|
+
return pn.FlatList(items, render_item=Row, ref=list_ref)
|
|
633
|
+
```
|
|
634
|
+
"""
|
|
635
|
+
ctx = _get_hook_state()
|
|
636
|
+
if ctx is None:
|
|
637
|
+
raise RuntimeError("use_layout_effect must be called inside a @component function")
|
|
638
|
+
ctx.record_hook("use_layout_effect")
|
|
639
|
+
|
|
640
|
+
idx = ctx.layout_effect_index
|
|
641
|
+
ctx.layout_effect_index += 1
|
|
642
|
+
|
|
643
|
+
if idx >= len(ctx.layout_effects):
|
|
644
|
+
ctx.layout_effects.append((_SENTINEL, None))
|
|
645
|
+
ctx._pending_layout_effects.append((idx, effect, deps))
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
prev_deps, _prev_cleanup = ctx.layout_effects[idx]
|
|
649
|
+
if _deps_changed(prev_deps, deps):
|
|
650
|
+
ctx._pending_layout_effects.append((idx, effect, deps))
|
|
651
|
+
|
|
652
|
+
|
|
433
653
|
def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
434
654
|
"""Return a memoized value that is recomputed only when `deps` change.
|
|
435
655
|
|
|
@@ -451,6 +671,7 @@ def use_memo(factory: Callable[[], T], deps: list) -> T:
|
|
|
451
671
|
ctx = _get_hook_state()
|
|
452
672
|
if ctx is None:
|
|
453
673
|
raise RuntimeError("use_memo must be called inside a @component function")
|
|
674
|
+
ctx.record_hook("use_memo")
|
|
454
675
|
|
|
455
676
|
idx = ctx.memo_index
|
|
456
677
|
ctx.memo_index += 1
|
|
@@ -486,23 +707,26 @@ def use_callback(callback: Callable, deps: list) -> Callable:
|
|
|
486
707
|
return use_memo(lambda: callback, deps)
|
|
487
708
|
|
|
488
709
|
|
|
489
|
-
def use_ref(initial:
|
|
490
|
-
"""Return a
|
|
710
|
+
def use_ref(initial: Optional[T] = None) -> Ref[T]:
|
|
711
|
+
"""Return a [`Ref`][pythonnative.Ref] that persists across renders.
|
|
491
712
|
|
|
492
713
|
Refs are useful for storing values that must survive renders without
|
|
493
714
|
triggering them: timers, last-seen values, native handles, and so on.
|
|
494
715
|
|
|
495
|
-
|
|
716
|
+
``ref.current`` is also populated by the reconciler with the
|
|
496
717
|
underlying native view (`UIView` on iOS, `android.view.View` on
|
|
497
|
-
Android) when the ref is passed via the
|
|
498
|
-
|
|
499
|
-
|
|
718
|
+
Android, a Tk widget on desktop) when the ref is passed via the
|
|
719
|
+
``ref=`` prop on a built-in element, and cleared to ``None`` when
|
|
720
|
+
that element unmounts. Composite components such as
|
|
721
|
+
[`FlatList`][pythonnative.FlatList] publish a typed controller
|
|
722
|
+
object instead (see
|
|
723
|
+
[`use_imperative_handle`][pythonnative.use_imperative_handle]).
|
|
500
724
|
|
|
501
725
|
Args:
|
|
502
|
-
initial: Value placed at
|
|
726
|
+
initial: Value placed at ``ref.current`` on first render.
|
|
503
727
|
|
|
504
728
|
Returns:
|
|
505
|
-
A
|
|
729
|
+
A [`Ref`][pythonnative.Ref]. Mutations to ``ref.current`` do
|
|
506
730
|
*not* trigger re-renders.
|
|
507
731
|
|
|
508
732
|
Raises:
|
|
@@ -511,18 +735,69 @@ def use_ref(initial: Any = None) -> dict:
|
|
|
511
735
|
ctx = _get_hook_state()
|
|
512
736
|
if ctx is None:
|
|
513
737
|
raise RuntimeError("use_ref must be called inside a @component function")
|
|
738
|
+
ctx.record_hook("use_ref")
|
|
514
739
|
|
|
515
740
|
idx = ctx.ref_index
|
|
516
741
|
ctx.ref_index += 1
|
|
517
742
|
|
|
518
743
|
if idx >= len(ctx.refs):
|
|
519
|
-
ref:
|
|
744
|
+
ref: Ref[T] = Ref(initial)
|
|
520
745
|
ctx.refs.append(ref)
|
|
521
746
|
return ref
|
|
522
747
|
|
|
523
748
|
return ctx.refs[idx]
|
|
524
749
|
|
|
525
750
|
|
|
751
|
+
def use_imperative_handle(
|
|
752
|
+
ref: Optional[Ref],
|
|
753
|
+
factory: Callable[[], Any],
|
|
754
|
+
deps: Optional[list] = None,
|
|
755
|
+
) -> None:
|
|
756
|
+
"""Publish a controller object on ``ref.current``.
|
|
757
|
+
|
|
758
|
+
The composite-component counterpart to passing ``ref=`` to a
|
|
759
|
+
built-in element. Call it inside a component that accepts a
|
|
760
|
+
``ref`` prop to expose a curated imperative API (rather than the
|
|
761
|
+
raw native view) to the parent. The handle is installed during the
|
|
762
|
+
commit's layout-effect phase and cleared back to ``None`` on
|
|
763
|
+
unmount.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
ref: The [`Ref`][pythonnative.Ref] received via the component's
|
|
767
|
+
``ref`` prop. ``None`` is allowed (the parent didn't
|
|
768
|
+
request a handle), in which case this is a no-op.
|
|
769
|
+
factory: Zero-arg callable returning the handle object.
|
|
770
|
+
deps: Dependency list controlling when the handle is rebuilt.
|
|
771
|
+
Defaults to ``[]`` semantics only if you pass ``[]``;
|
|
772
|
+
``None`` rebuilds on every render, matching effects.
|
|
773
|
+
|
|
774
|
+
Raises:
|
|
775
|
+
RuntimeError: If called outside a `@component` function.
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
```python
|
|
779
|
+
import pythonnative as pn
|
|
780
|
+
|
|
781
|
+
@pn.component
|
|
782
|
+
def VideoPlayer(source, ref=None):
|
|
783
|
+
pn.use_imperative_handle(ref, lambda: PlayerController(...), [source])
|
|
784
|
+
return pn.View(...)
|
|
785
|
+
```
|
|
786
|
+
"""
|
|
787
|
+
|
|
788
|
+
def _install() -> Optional[Callable[[], None]]:
|
|
789
|
+
if ref is None:
|
|
790
|
+
return None
|
|
791
|
+
ref.current = factory()
|
|
792
|
+
|
|
793
|
+
def _clear() -> None:
|
|
794
|
+
ref.current = None
|
|
795
|
+
|
|
796
|
+
return _clear
|
|
797
|
+
|
|
798
|
+
use_layout_effect(_install, deps)
|
|
799
|
+
|
|
800
|
+
|
|
526
801
|
# ======================================================================
|
|
527
802
|
# Async hooks
|
|
528
803
|
# ======================================================================
|
|
@@ -570,6 +845,22 @@ def use_async_effect(
|
|
|
570
845
|
def _sync_effect() -> Callable[[], None]:
|
|
571
846
|
future = run_async(effect())
|
|
572
847
|
|
|
848
|
+
def _observe(fut: Any) -> None:
|
|
849
|
+
# Surface unhandled async-effect crashes instead of letting
|
|
850
|
+
# the future's exception vanish unobserved: RedBox in dev
|
|
851
|
+
# mode, traceback in production.
|
|
852
|
+
if fut.cancelled():
|
|
853
|
+
return
|
|
854
|
+
exc = fut.exception()
|
|
855
|
+
if exc is None or isinstance(exc, asyncio.CancelledError):
|
|
856
|
+
return
|
|
857
|
+
if not diagnostics.report_error(exc, phase="async effect"):
|
|
858
|
+
import traceback
|
|
859
|
+
|
|
860
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
861
|
+
|
|
862
|
+
future.add_done_callback(_observe)
|
|
863
|
+
|
|
573
864
|
def _cancel() -> None:
|
|
574
865
|
future.cancel()
|
|
575
866
|
|
|
@@ -704,7 +995,7 @@ class MutationCall(Generic[T]):
|
|
|
704
995
|
Example:
|
|
705
996
|
```python
|
|
706
997
|
# Fire-and-forget:
|
|
707
|
-
save_button.
|
|
998
|
+
save_button.on_press = lambda: mutate(post)
|
|
708
999
|
|
|
709
1000
|
# Or await for the result:
|
|
710
1001
|
async def submit():
|
|
@@ -760,7 +1051,7 @@ def use_mutation(
|
|
|
760
1051
|
state, save = pn.use_mutation(api.create_post)
|
|
761
1052
|
|
|
762
1053
|
return pn.Column(
|
|
763
|
-
pn.Button("Save",
|
|
1054
|
+
pn.Button("Save", on_press=lambda: save(post)),
|
|
764
1055
|
pn.Text("Saving…") if state.loading else pn.Text(""),
|
|
765
1056
|
pn.Text(str(state.error)) if state.error else pn.Text(""),
|
|
766
1057
|
)
|
|
@@ -899,6 +1190,47 @@ def use_keyboard_height() -> float:
|
|
|
899
1190
|
return platform_metrics.get_keyboard_height()
|
|
900
1191
|
|
|
901
1192
|
|
|
1193
|
+
def use_color_scheme() -> str:
|
|
1194
|
+
"""Return the effective color scheme and re-render when it changes.
|
|
1195
|
+
|
|
1196
|
+
Equivalent to React Native's ``useColorScheme``. The system value
|
|
1197
|
+
is published by the screen host; an app-level override set through
|
|
1198
|
+
[`appearance.set_color_scheme`][pythonnative.appearance.set_color_scheme]
|
|
1199
|
+
takes precedence.
|
|
1200
|
+
|
|
1201
|
+
Returns:
|
|
1202
|
+
``"light"`` or ``"dark"``.
|
|
1203
|
+
|
|
1204
|
+
Raises:
|
|
1205
|
+
RuntimeError: If called outside a `@component` function.
|
|
1206
|
+
|
|
1207
|
+
Example:
|
|
1208
|
+
```python
|
|
1209
|
+
import pythonnative as pn
|
|
1210
|
+
|
|
1211
|
+
@pn.component
|
|
1212
|
+
def Banner():
|
|
1213
|
+
scheme = pn.use_color_scheme()
|
|
1214
|
+
bg = "#000000" if scheme == "dark" else "#FFFFFF"
|
|
1215
|
+
return pn.View(style=pn.style(background_color=bg))
|
|
1216
|
+
```
|
|
1217
|
+
"""
|
|
1218
|
+
from . import appearance
|
|
1219
|
+
|
|
1220
|
+
ctx = _get_hook_state()
|
|
1221
|
+
if ctx is None:
|
|
1222
|
+
raise RuntimeError("use_color_scheme must be called inside a @component function")
|
|
1223
|
+
|
|
1224
|
+
_, set_tick = use_state(0)
|
|
1225
|
+
|
|
1226
|
+
def subscribe() -> Callable[[], None]:
|
|
1227
|
+
return appearance.subscribe(lambda: set_tick(lambda n: n + 1))
|
|
1228
|
+
|
|
1229
|
+
use_effect(subscribe, [])
|
|
1230
|
+
|
|
1231
|
+
return appearance.get_color_scheme()
|
|
1232
|
+
|
|
1233
|
+
|
|
902
1234
|
# ======================================================================
|
|
903
1235
|
# Context
|
|
904
1236
|
# ======================================================================
|
|
@@ -911,6 +1243,10 @@ class Context:
|
|
|
911
1243
|
via [`use_context`][pythonnative.use_context]. Use
|
|
912
1244
|
[`Provider`][pythonnative.Provider] to set the value for a subtree.
|
|
913
1245
|
|
|
1246
|
+
Context is *reactive*: when a Provider's value changes, every
|
|
1247
|
+
component that read the context on its last render re-renders,
|
|
1248
|
+
even if a memoized ancestor skipped its own re-render.
|
|
1249
|
+
|
|
914
1250
|
Attributes:
|
|
915
1251
|
default: The value returned when no `Provider` ancestor exists.
|
|
916
1252
|
"""
|
|
@@ -948,6 +1284,9 @@ def use_context(context: Context) -> Any:
|
|
|
948
1284
|
"""Read the current value of `context` from the nearest `Provider`.
|
|
949
1285
|
|
|
950
1286
|
If no enclosing `Provider` exists, returns the context's default.
|
|
1287
|
+
The component is registered as a subscriber: when the nearest
|
|
1288
|
+
Provider's value changes, the component re-renders even if a
|
|
1289
|
+
memoized ancestor skipped.
|
|
951
1290
|
|
|
952
1291
|
Args:
|
|
953
1292
|
context: The `Context` to read from.
|
|
@@ -961,7 +1300,10 @@ def use_context(context: Context) -> Any:
|
|
|
961
1300
|
ctx = _get_hook_state()
|
|
962
1301
|
if ctx is None:
|
|
963
1302
|
raise RuntimeError("use_context must be called inside a @component function")
|
|
964
|
-
|
|
1303
|
+
ctx.record_hook("use_context")
|
|
1304
|
+
value = context._current()
|
|
1305
|
+
ctx.context_deps[id(context)] = value
|
|
1306
|
+
return value
|
|
965
1307
|
|
|
966
1308
|
|
|
967
1309
|
# ======================================================================
|
|
@@ -972,10 +1314,14 @@ def use_context(context: Context) -> Any:
|
|
|
972
1314
|
def Provider(context: "Context", value: Any, *children: Element) -> Element:
|
|
973
1315
|
"""Provide ``value`` for ``context`` to all descendants of ``children``.
|
|
974
1316
|
|
|
975
|
-
Accepts any number of children (varargs).
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1317
|
+
Accepts any number of children (varargs). A Provider contributes no
|
|
1318
|
+
native view of its own; its children mount directly into the
|
|
1319
|
+
surrounding native parent.
|
|
1320
|
+
|
|
1321
|
+
When ``value`` differs from the previous render (identity, then
|
|
1322
|
+
``==``), every descendant that read the context via
|
|
1323
|
+
[`use_context`][pythonnative.use_context] re-renders, including
|
|
1324
|
+
descendants of memoized components that skipped.
|
|
979
1325
|
|
|
980
1326
|
Args:
|
|
981
1327
|
context: The [`Context`][pythonnative.hooks.Context] to set.
|
|
@@ -1003,13 +1349,7 @@ def Provider(context: "Context", value: Any, *children: Element) -> Element:
|
|
|
1003
1349
|
)
|
|
1004
1350
|
```
|
|
1005
1351
|
"""
|
|
1006
|
-
|
|
1007
|
-
kids: List[Element] = []
|
|
1008
|
-
elif len(children) == 1:
|
|
1009
|
-
kids = [children[0]]
|
|
1010
|
-
else:
|
|
1011
|
-
kids = [Element("__Fragment__", {}, list(children))]
|
|
1012
|
-
return Element("__Provider__", {"__context__": context, "__value__": value}, kids)
|
|
1352
|
+
return Element("__Provider__", {"__context__": context, "__value__": value}, list(children))
|
|
1013
1353
|
|
|
1014
1354
|
|
|
1015
1355
|
def memo(component_fn: Callable[..., Element]) -> Callable[..., Element]:
|
|
@@ -1082,7 +1422,7 @@ class NavigationHandle:
|
|
|
1082
1422
|
nav = pn.use_navigation()
|
|
1083
1423
|
return pn.Button(
|
|
1084
1424
|
"Open Detail",
|
|
1085
|
-
|
|
1425
|
+
on_press=lambda: nav.navigate("Detail", {"id": 42}),
|
|
1086
1426
|
)
|
|
1087
1427
|
```
|
|
1088
1428
|
"""
|
|
@@ -1139,6 +1479,71 @@ def use_navigation() -> NavigationHandle:
|
|
|
1139
1479
|
return handle
|
|
1140
1480
|
|
|
1141
1481
|
|
|
1482
|
+
def use_back_handler(handler: Callable[[], bool]) -> None:
|
|
1483
|
+
"""Intercept the system back action for this screen.
|
|
1484
|
+
|
|
1485
|
+
On Android this handles the hardware back button and predictive
|
|
1486
|
+
back gesture; in the desktop preview it handles the Escape key.
|
|
1487
|
+
iOS has no system back button, so the handler never fires there
|
|
1488
|
+
(swipe-back is controlled by the navigation stack instead).
|
|
1489
|
+
|
|
1490
|
+
Handlers registered later run first, so a component mounted on top
|
|
1491
|
+
of existing content (a modal, a confirmation sheet) takes priority
|
|
1492
|
+
over handlers that were already mounted. Return ``True`` to consume
|
|
1493
|
+
the event and stop both remaining handlers and the platform's
|
|
1494
|
+
default behavior (popping the screen); return ``False`` to pass it
|
|
1495
|
+
along.
|
|
1496
|
+
|
|
1497
|
+
The latest ``handler`` closure from the most recent render is
|
|
1498
|
+
always the one invoked; registration order is fixed at mount, so
|
|
1499
|
+
re-renders never change priority.
|
|
1500
|
+
|
|
1501
|
+
Args:
|
|
1502
|
+
handler: Zero-arg callable returning ``True`` if it consumed
|
|
1503
|
+
the back action.
|
|
1504
|
+
|
|
1505
|
+
Raises:
|
|
1506
|
+
RuntimeError: If called outside a `@component` function.
|
|
1507
|
+
|
|
1508
|
+
Example:
|
|
1509
|
+
```python
|
|
1510
|
+
import pythonnative as pn
|
|
1511
|
+
|
|
1512
|
+
@pn.component
|
|
1513
|
+
def Editor():
|
|
1514
|
+
dirty, set_dirty = pn.use_state(False)
|
|
1515
|
+
pn.use_back_handler(lambda: dirty) # block back while dirty
|
|
1516
|
+
...
|
|
1517
|
+
```
|
|
1518
|
+
"""
|
|
1519
|
+
ctx = _get_hook_state()
|
|
1520
|
+
if ctx is None:
|
|
1521
|
+
raise RuntimeError("use_back_handler must be called inside a @component function")
|
|
1522
|
+
|
|
1523
|
+
latest: Ref[Callable[[], bool]] = use_ref(handler)
|
|
1524
|
+
latest.current = handler
|
|
1525
|
+
|
|
1526
|
+
def _register() -> Optional[Callable[[], None]]:
|
|
1527
|
+
reconciler = ctx._reconciler
|
|
1528
|
+
if reconciler is None or not hasattr(reconciler, "register_back_handler"):
|
|
1529
|
+
return None
|
|
1530
|
+
|
|
1531
|
+
def _trampoline() -> bool:
|
|
1532
|
+
fn = latest.current
|
|
1533
|
+
if fn is None:
|
|
1534
|
+
return False
|
|
1535
|
+
try:
|
|
1536
|
+
return bool(fn())
|
|
1537
|
+
except Exception as exc:
|
|
1538
|
+
if not diagnostics.report_error(exc, phase="back handler"):
|
|
1539
|
+
raise
|
|
1540
|
+
return True
|
|
1541
|
+
|
|
1542
|
+
return reconciler.register_back_handler(_trampoline)
|
|
1543
|
+
|
|
1544
|
+
use_effect(_register, [])
|
|
1545
|
+
|
|
1546
|
+
|
|
1142
1547
|
# ======================================================================
|
|
1143
1548
|
# @component decorator
|
|
1144
1549
|
# ======================================================================
|