pythonnative 0.24.0__py3-none-any.whl → 0.25.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonnative/__init__.py +16 -4
- pythonnative/animated.py +2 -2
- pythonnative/components.py +59 -0
- pythonnative/hooks.py +364 -81
- pythonnative/native_modules/notifications.py +36 -4
- pythonnative/native_modules/permissions.py +24 -1
- pythonnative/native_views/android.py +7 -2
- pythonnative/native_views/ios.py +12 -1
- pythonnative/reconciler.py +518 -37
- pythonnative/runtime.py +407 -215
- pythonnative/screen.py +53 -7
- pythonnative/storage.py +2 -2
- pythonnative/suspense.py +417 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/METADATA +2 -1
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/RECORD +19 -18
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/WHEEL +1 -1
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.25.0.dist-info}/top_level.txt +0 -0
pythonnative/screen.py
CHANGED
|
@@ -1587,6 +1587,19 @@ else:
|
|
|
1587
1587
|
return True
|
|
1588
1588
|
_log_pn("_request_render: native iOS scheduler unavailable; render remains queued")
|
|
1589
1589
|
return True
|
|
1590
|
+
# Defer via the main dispatch queue rather than an NSTimer.
|
|
1591
|
+
# GCD main-queue blocks are serviced in the common run-loop
|
|
1592
|
+
# modes, so they still run while UIKit is in tracking mode
|
|
1593
|
+
# (scrolls, synthesized test touches). A default-mode NSTimer
|
|
1594
|
+
# is starved for the whole gesture, which delayed renders
|
|
1595
|
+
# long enough on slow CI simulators for Maestro asserts to
|
|
1596
|
+
# time out, and let stale flushes land mid-touch.
|
|
1597
|
+
from .runtime import _ensure_libdispatch_loaded, _ios_dispatch_async
|
|
1598
|
+
|
|
1599
|
+
if _ensure_libdispatch_loaded():
|
|
1600
|
+
_ios_dispatch_async(_flush_ios_scheduled_renders)
|
|
1601
|
+
_log_pn("_request_render: deferred iOS render to next main-queue turn")
|
|
1602
|
+
return True
|
|
1590
1603
|
try:
|
|
1591
1604
|
NSTimer = ObjCClass("NSTimer")
|
|
1592
1605
|
target = _ensure_ios_render_scheduler_target()
|
|
@@ -1719,22 +1732,55 @@ else:
|
|
|
1719
1732
|
raise RuntimeError(
|
|
1720
1733
|
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
1721
1734
|
)
|
|
1722
|
-
|
|
1735
|
+
self._run_nav_op(lambda n: n.pushViewController_animated_(next_vc, True))
|
|
1723
1736
|
|
|
1724
1737
|
def _pop(self) -> None:
|
|
1725
|
-
|
|
1726
|
-
if nav is not None:
|
|
1727
|
-
nav.popViewControllerAnimated_(True)
|
|
1738
|
+
self._run_nav_op(lambda n: n.popViewControllerAnimated_(True))
|
|
1728
1739
|
|
|
1729
1740
|
def _reset_to_root(self) -> None:
|
|
1730
1741
|
"""Pop everything above the root view-controller."""
|
|
1731
|
-
|
|
1732
|
-
|
|
1742
|
+
|
|
1743
|
+
def op(n: Any) -> None:
|
|
1733
1744
|
try:
|
|
1734
|
-
|
|
1745
|
+
n.popToRootViewControllerAnimated_(True)
|
|
1735
1746
|
except Exception:
|
|
1736
1747
|
pass
|
|
1737
1748
|
|
|
1749
|
+
self._run_nav_op(op)
|
|
1750
|
+
|
|
1751
|
+
def _run_nav_op(self, op: Any) -> None:
|
|
1752
|
+
"""Run ``op(nav)`` unless a stack transition is mid-flight.
|
|
1753
|
+
|
|
1754
|
+
UIKit does not queue navigation calls: a
|
|
1755
|
+
``pushViewController`` / ``popViewController`` issued
|
|
1756
|
+
while the previous transition is still animating is
|
|
1757
|
+
silently ignored or, worse, misapplies and pops past
|
|
1758
|
+
the intended screen. A fast tap right after a push
|
|
1759
|
+
(easy for a user, common for a UI test on a slow
|
|
1760
|
+
machine) lands exactly in that window. Dropping the
|
|
1761
|
+
operation keeps the stack consistent: the tap simply
|
|
1762
|
+
does nothing, and a retry lands on a settled stack.
|
|
1763
|
+
(Deferring the operation instead was tried and is
|
|
1764
|
+
worse: the user retries, then the deferred operation
|
|
1765
|
+
also fires, popping one screen too many.)
|
|
1766
|
+
"""
|
|
1767
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
1768
|
+
if nav is None:
|
|
1769
|
+
return
|
|
1770
|
+
try:
|
|
1771
|
+
# rubicon resolves ``transitionCoordinator`` as a bound
|
|
1772
|
+
# method rather than a property on some classes; call
|
|
1773
|
+
# it to get the actual (possibly nil -> None) value.
|
|
1774
|
+
coord = nav.transitionCoordinator
|
|
1775
|
+
if callable(coord):
|
|
1776
|
+
coord = coord()
|
|
1777
|
+
if coord is not None:
|
|
1778
|
+
_log_pn("_run_nav_op: transition in flight; dropping nav op")
|
|
1779
|
+
return
|
|
1780
|
+
except Exception:
|
|
1781
|
+
pass
|
|
1782
|
+
op(nav)
|
|
1783
|
+
|
|
1738
1784
|
def _set_screen_options(self, options: Dict[str, Any]) -> None:
|
|
1739
1785
|
"""Bind screen options (e.g. title) to the native nav bar.
|
|
1740
1786
|
|
pythonnative/storage.py
CHANGED
|
@@ -379,7 +379,7 @@ def use_persisted_state(
|
|
|
379
379
|
)
|
|
380
380
|
```
|
|
381
381
|
"""
|
|
382
|
-
from .hooks import
|
|
382
|
+
from .hooks import use_callback, use_effect, use_ref, use_state
|
|
383
383
|
from .runtime import run_async
|
|
384
384
|
|
|
385
385
|
state, set_state = use_state(initial)
|
|
@@ -391,7 +391,7 @@ def use_persisted_state(
|
|
|
391
391
|
set_state(stored)
|
|
392
392
|
loaded.current = True
|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
use_effect(_load, [key])
|
|
395
395
|
|
|
396
396
|
def setter(value_or_updater: Any) -> None:
|
|
397
397
|
def _reducer(current: Any) -> Any:
|
pythonnative/suspense.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Suspense primitives: suspension signals, resources, and lazy loading.
|
|
2
|
+
|
|
3
|
+
This module implements the machinery behind PythonNative's async
|
|
4
|
+
rendering model:
|
|
5
|
+
|
|
6
|
+
- ``async def`` components: a component body may be a coroutine. The
|
|
7
|
+
reconciler drives it synchronously as far as possible (awaits on
|
|
8
|
+
already-resolved futures complete inline); if it blocks on pending
|
|
9
|
+
work, the render **suspends** and the nearest
|
|
10
|
+
[`Suspense`][pythonnative.Suspense] boundary shows its fallback
|
|
11
|
+
until the coroutine finishes.
|
|
12
|
+
- [`use_resource`][pythonnative.use_resource]: a hook that starts an
|
|
13
|
+
async fetch and caches it across renders. Reading an unresolved
|
|
14
|
+
[`Resource`][pythonnative.Resource] suspends the render; awaiting it
|
|
15
|
+
inside an ``async def`` component does the same thing without the
|
|
16
|
+
boundary having to re-run anything by hand.
|
|
17
|
+
- [`lazy`][pythonnative.lazy]: code-splitting for components. The
|
|
18
|
+
loader runs once, and renders suspend until it resolves.
|
|
19
|
+
|
|
20
|
+
The core building block is [`CoroDriver`][pythonnative.suspense.CoroDriver],
|
|
21
|
+
a miniature task that steps a coroutine **synchronously** until it
|
|
22
|
+
either finishes or blocks on a pending :class:`asyncio.Future`. This is
|
|
23
|
+
what lets an ``async def`` component whose awaits are all cached
|
|
24
|
+
(resolved resources, completed tasks) render in a single synchronous
|
|
25
|
+
pass with zero event-loop round trips, while genuinely pending awaits
|
|
26
|
+
suspend cleanly.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import contextvars
|
|
33
|
+
import inspect
|
|
34
|
+
from typing import Any, Callable, Generic, List, Optional, Tuple, TypeVar
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
|
|
38
|
+
_UNSET = object()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Suspend(BaseException):
|
|
42
|
+
"""Signal that a render is blocked on pending async work.
|
|
43
|
+
|
|
44
|
+
Raised while a component renders, either by
|
|
45
|
+
[`Resource.read`][pythonnative.suspense.Resource.read] or by an
|
|
46
|
+
``async def`` component body blocking on a pending await. The
|
|
47
|
+
reconciler catches it: the nearest
|
|
48
|
+
[`Suspense`][pythonnative.Suspense] boundary shows its fallback
|
|
49
|
+
(initial mounts), or the component keeps its previous content and
|
|
50
|
+
re-renders when the work finishes (updates).
|
|
51
|
+
|
|
52
|
+
Derives from :class:`BaseException` so
|
|
53
|
+
[`ErrorBoundary`][pythonnative.ErrorBoundary] (which catches
|
|
54
|
+
:class:`Exception`) never mistakes a suspension for a crash.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
waitable: The pending work; exposes ``done()`` and
|
|
58
|
+
``add_done_callback(cb)``.
|
|
59
|
+
hook_state: The suspended component's hook state, carried so
|
|
60
|
+
a Suspense boundary can preserve it across retries (its
|
|
61
|
+
cached resources survive, so the retry doesn't refetch).
|
|
62
|
+
key: ``(component identity, element key)`` used to re-match
|
|
63
|
+
``hook_state`` on retry.
|
|
64
|
+
label: Component name for diagnostics.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, waitable: Any, hook_state: Any = None, label: str = "") -> None:
|
|
68
|
+
super().__init__(label or "render suspended")
|
|
69
|
+
self.waitable = waitable
|
|
70
|
+
self.hook_state = hook_state
|
|
71
|
+
self.key: Optional[Tuple[int, Any]] = None
|
|
72
|
+
self.label = label
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CoroDriver:
|
|
76
|
+
"""Drive a coroutine synchronously until it blocks on pending work.
|
|
77
|
+
|
|
78
|
+
A miniature :class:`asyncio.Task`: it steps the coroutine with
|
|
79
|
+
``send()``, resolving awaits on already-done futures inline. When
|
|
80
|
+
the coroutine blocks on a *pending* future, the driver parks and
|
|
81
|
+
resumes from that future's done callback (on the framework loop's
|
|
82
|
+
thread). Each step runs inside the :mod:`contextvars` context
|
|
83
|
+
captured at creation, so hooks called after an ``await`` still see
|
|
84
|
+
the owning component's hook state.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
done: Whether the coroutine finished (returned, raised, or was
|
|
88
|
+
cancelled).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
__slots__ = (
|
|
92
|
+
"_coro",
|
|
93
|
+
"_context",
|
|
94
|
+
"done",
|
|
95
|
+
"_cancelled",
|
|
96
|
+
"_result",
|
|
97
|
+
"_error",
|
|
98
|
+
"_callbacks",
|
|
99
|
+
"_waiting_on",
|
|
100
|
+
"_stepping",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def __init__(self, coro: Any, context: Optional[contextvars.Context] = None) -> None:
|
|
104
|
+
self._coro = coro
|
|
105
|
+
self._context = context if context is not None else contextvars.copy_context()
|
|
106
|
+
self.done = False
|
|
107
|
+
self._cancelled = False
|
|
108
|
+
self._result: Any = _UNSET
|
|
109
|
+
self._error: Optional[BaseException] = None
|
|
110
|
+
self._callbacks: List[Callable[["CoroDriver"], None]] = []
|
|
111
|
+
self._waiting_on: Optional[Any] = None
|
|
112
|
+
self._stepping = False
|
|
113
|
+
|
|
114
|
+
# -- inspection ------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def cancelled(self) -> bool:
|
|
117
|
+
"""Whether the driver was cancelled before finishing normally."""
|
|
118
|
+
return self._cancelled
|
|
119
|
+
|
|
120
|
+
def result(self) -> Any:
|
|
121
|
+
"""Return the coroutine's return value (raises if not done or failed)."""
|
|
122
|
+
if not self.done:
|
|
123
|
+
raise RuntimeError("CoroDriver is not done")
|
|
124
|
+
if self._error is not None:
|
|
125
|
+
raise self._error
|
|
126
|
+
return self._result
|
|
127
|
+
|
|
128
|
+
def exception(self) -> Optional[BaseException]:
|
|
129
|
+
"""Return the coroutine's exception, or ``None``."""
|
|
130
|
+
if not self.done:
|
|
131
|
+
raise RuntimeError("CoroDriver is not done")
|
|
132
|
+
return self._error
|
|
133
|
+
|
|
134
|
+
def add_done_callback(self, callback: Callable[["CoroDriver"], None]) -> None:
|
|
135
|
+
"""Invoke ``callback(self)`` when the coroutine finishes.
|
|
136
|
+
|
|
137
|
+
Fires immediately when already done. Callbacks run on whatever
|
|
138
|
+
thread finished the coroutine (the framework loop's thread for
|
|
139
|
+
suspended coroutines).
|
|
140
|
+
"""
|
|
141
|
+
if self.done:
|
|
142
|
+
callback(self)
|
|
143
|
+
return
|
|
144
|
+
self._callbacks.append(callback)
|
|
145
|
+
|
|
146
|
+
# -- driving ---------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def start(self) -> None:
|
|
149
|
+
"""Run the coroutine as far as it can go without blocking."""
|
|
150
|
+
self._step(("send", None))
|
|
151
|
+
|
|
152
|
+
def cancel(self) -> bool:
|
|
153
|
+
"""Throw :class:`asyncio.CancelledError` into the coroutine.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
``False`` when the coroutine had already finished,
|
|
157
|
+
``True`` otherwise.
|
|
158
|
+
"""
|
|
159
|
+
if self.done:
|
|
160
|
+
return False
|
|
161
|
+
waiting = self._waiting_on
|
|
162
|
+
self._waiting_on = None
|
|
163
|
+
if waiting is not None:
|
|
164
|
+
try:
|
|
165
|
+
waiting.remove_done_callback(self._resume)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
self._cancelled = True
|
|
169
|
+
self._step(("throw", asyncio.CancelledError()))
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
def _resume(self, _fut: Any) -> None:
|
|
173
|
+
self._waiting_on = None
|
|
174
|
+
self._step(("send", None))
|
|
175
|
+
|
|
176
|
+
def _step(self, action: Tuple[str, Any]) -> None:
|
|
177
|
+
if self.done or self._stepping:
|
|
178
|
+
return
|
|
179
|
+
self._stepping = True
|
|
180
|
+
# When a step runs outside a loop iteration (a synchronous
|
|
181
|
+
# render), mark the framework loop as the running loop for the
|
|
182
|
+
# duration, exactly as asyncio.Task steps do. Without this,
|
|
183
|
+
# anything the coroutine calls that uses
|
|
184
|
+
# ``asyncio.get_running_loop()`` internally (``asyncio.sleep``,
|
|
185
|
+
# ``asyncio.create_task``, ...) raises "no running event loop".
|
|
186
|
+
from . import runtime
|
|
187
|
+
|
|
188
|
+
events = asyncio.events
|
|
189
|
+
installed_loop = None
|
|
190
|
+
if events._get_running_loop() is None:
|
|
191
|
+
installed_loop = runtime.get_loop()
|
|
192
|
+
events._set_running_loop(installed_loop)
|
|
193
|
+
try:
|
|
194
|
+
self._context.run(self._advance, action)
|
|
195
|
+
finally:
|
|
196
|
+
if installed_loop is not None:
|
|
197
|
+
events._set_running_loop(None)
|
|
198
|
+
self._stepping = False
|
|
199
|
+
|
|
200
|
+
def _advance(self, action: Tuple[str, Any]) -> None:
|
|
201
|
+
kind, value = action
|
|
202
|
+
while True:
|
|
203
|
+
try:
|
|
204
|
+
if kind == "throw":
|
|
205
|
+
yielded = self._coro.throw(value)
|
|
206
|
+
else:
|
|
207
|
+
yielded = self._coro.send(value)
|
|
208
|
+
except StopIteration as stop:
|
|
209
|
+
self._finish(stop.value, None)
|
|
210
|
+
return
|
|
211
|
+
except asyncio.CancelledError as exc:
|
|
212
|
+
self._cancelled = True
|
|
213
|
+
self._finish(None, exc)
|
|
214
|
+
return
|
|
215
|
+
except BaseException as exc:
|
|
216
|
+
self._finish(None, exc)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
kind, value = "send", None
|
|
220
|
+
if yielded is None:
|
|
221
|
+
# ``await asyncio.sleep(0)`` and bare yields: resume on
|
|
222
|
+
# the next loop tick to preserve cooperative yielding.
|
|
223
|
+
from . import runtime
|
|
224
|
+
|
|
225
|
+
runtime.get_loop().call_soon(self._resume, None)
|
|
226
|
+
return
|
|
227
|
+
if isinstance(yielded, asyncio.Future):
|
|
228
|
+
# Mirror asyncio.Task's handshake with Future.__await__.
|
|
229
|
+
try:
|
|
230
|
+
yielded._asyncio_future_blocking = False
|
|
231
|
+
except AttributeError:
|
|
232
|
+
pass
|
|
233
|
+
if yielded.done():
|
|
234
|
+
continue
|
|
235
|
+
self._waiting_on = yielded
|
|
236
|
+
yielded.add_done_callback(self._resume)
|
|
237
|
+
return
|
|
238
|
+
self._finish(
|
|
239
|
+
None,
|
|
240
|
+
RuntimeError(
|
|
241
|
+
f"async component awaited an unsupported object: {yielded!r}. "
|
|
242
|
+
"Await asyncio futures, tasks, or PythonNative awaitables."
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
def _finish(self, result: Any, error: Optional[BaseException]) -> None:
|
|
248
|
+
self.done = True
|
|
249
|
+
self._result = result
|
|
250
|
+
self._error = error
|
|
251
|
+
self._waiting_on = None
|
|
252
|
+
callbacks = self._callbacks
|
|
253
|
+
self._callbacks = []
|
|
254
|
+
for callback in callbacks:
|
|
255
|
+
try:
|
|
256
|
+
callback(self)
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
print(f"[pn.suspense] done callback raised: {exc!r}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Resource(Generic[T]):
|
|
262
|
+
"""A cached async value with Suspense integration.
|
|
263
|
+
|
|
264
|
+
Returned by [`use_resource`][pythonnative.use_resource]. A resource
|
|
265
|
+
starts fetching as soon as the hook runs and remembers its result
|
|
266
|
+
across renders (until its dependencies change), so re-renders never
|
|
267
|
+
refetch.
|
|
268
|
+
|
|
269
|
+
Two ways to consume it:
|
|
270
|
+
|
|
271
|
+
- ``resource.read()`` in a regular component: returns the value
|
|
272
|
+
when ready, re-raises the fetcher's error if it failed, and
|
|
273
|
+
**suspends the render** while pending.
|
|
274
|
+
- ``await resource`` in an ``async def`` component: same
|
|
275
|
+
semantics, expressed as a plain await.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
__slots__ = ("_driver",)
|
|
279
|
+
|
|
280
|
+
def __init__(self, driver: CoroDriver) -> None:
|
|
281
|
+
self._driver = driver
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def ready(self) -> bool:
|
|
285
|
+
"""Whether the fetch has finished (successfully or not)."""
|
|
286
|
+
return self._driver.done
|
|
287
|
+
|
|
288
|
+
def read(self) -> T:
|
|
289
|
+
"""Return the fetched value, or suspend the render while pending.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
Suspend: While the fetch is still in flight (caught by the
|
|
293
|
+
reconciler, never by user code).
|
|
294
|
+
BaseException: Whatever the fetcher raised, re-raised so an
|
|
295
|
+
enclosing [`ErrorBoundary`][pythonnative.ErrorBoundary]
|
|
296
|
+
can catch it.
|
|
297
|
+
"""
|
|
298
|
+
driver = self._driver
|
|
299
|
+
if driver.done:
|
|
300
|
+
return driver.result()
|
|
301
|
+
raise Suspend(driver, label="Resource.read")
|
|
302
|
+
|
|
303
|
+
def __await__(self) -> Any:
|
|
304
|
+
driver = self._driver
|
|
305
|
+
if driver.done:
|
|
306
|
+
|
|
307
|
+
def _done() -> Any:
|
|
308
|
+
return driver.result()
|
|
309
|
+
yield # pragma: no cover - marks this as a generator
|
|
310
|
+
|
|
311
|
+
return _done()
|
|
312
|
+
from . import runtime
|
|
313
|
+
|
|
314
|
+
future = runtime.create_future()
|
|
315
|
+
|
|
316
|
+
def _transfer(d: CoroDriver) -> None:
|
|
317
|
+
error = d.exception() if not d.cancelled() else asyncio.CancelledError()
|
|
318
|
+
if error is not None:
|
|
319
|
+
runtime.reject_future(future, error)
|
|
320
|
+
else:
|
|
321
|
+
runtime.resolve_future(future, d.result())
|
|
322
|
+
|
|
323
|
+
driver.add_done_callback(_transfer)
|
|
324
|
+
return future.__await__()
|
|
325
|
+
|
|
326
|
+
def cancel(self) -> None:
|
|
327
|
+
"""Cancel the in-flight fetch (no-op when already done)."""
|
|
328
|
+
self._driver.cancel()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def start_resource(fetcher: Callable[[], Any]) -> Resource[Any]:
|
|
332
|
+
"""Start ``fetcher`` immediately and wrap it in a [`Resource`][pythonnative.Resource].
|
|
333
|
+
|
|
334
|
+
The fetcher may be an ``async def`` (typical) or a plain function;
|
|
335
|
+
synchronous results resolve the resource immediately, so reading
|
|
336
|
+
it never suspends.
|
|
337
|
+
|
|
338
|
+
This is the non-hook constructor used for module-level resources
|
|
339
|
+
(preloading data before a screen mounts) and by
|
|
340
|
+
[`lazy`][pythonnative.lazy]. Inside components, prefer
|
|
341
|
+
[`use_resource`][pythonnative.use_resource], which caches per
|
|
342
|
+
component instance and re-fetches when dependencies change.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
async def _run() -> Any:
|
|
346
|
+
value = fetcher()
|
|
347
|
+
if inspect.isawaitable(value):
|
|
348
|
+
return await value
|
|
349
|
+
return value
|
|
350
|
+
|
|
351
|
+
driver = CoroDriver(_run())
|
|
352
|
+
driver.start()
|
|
353
|
+
return Resource(driver)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def lazy(loader: Callable[[], Any]) -> Callable[..., Any]:
|
|
357
|
+
"""Define a component that loads its implementation on first render.
|
|
358
|
+
|
|
359
|
+
``loader`` runs once, the first time the returned component
|
|
360
|
+
renders; until it resolves, renders suspend (so wrap usages in a
|
|
361
|
+
[`Suspense`][pythonnative.Suspense] boundary to show a loading
|
|
362
|
+
state). The loaded value must be a component (a ``@component``
|
|
363
|
+
function or any element factory).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
loader: A zero-arg callable returning the component, or an
|
|
367
|
+
``async def`` resolving to it. Synchronous loaders (a
|
|
368
|
+
deferred ``import``) resolve immediately and never
|
|
369
|
+
suspend.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
A component. Props (and children) pass through to the loaded
|
|
373
|
+
component unchanged.
|
|
374
|
+
|
|
375
|
+
Example:
|
|
376
|
+
```python
|
|
377
|
+
import pythonnative as pn
|
|
378
|
+
|
|
379
|
+
Chart = pn.lazy(lambda: __import__("app.chart", fromlist=["Chart"]).Chart)
|
|
380
|
+
|
|
381
|
+
@pn.component
|
|
382
|
+
def Dashboard():
|
|
383
|
+
return pn.Suspense(
|
|
384
|
+
Chart(points=[1, 2, 3]),
|
|
385
|
+
fallback=pn.ActivityIndicator(),
|
|
386
|
+
)
|
|
387
|
+
```
|
|
388
|
+
"""
|
|
389
|
+
from .hooks import component
|
|
390
|
+
|
|
391
|
+
state: dict = {}
|
|
392
|
+
|
|
393
|
+
@component
|
|
394
|
+
def Lazy(**props: Any) -> Any:
|
|
395
|
+
resource = state.get("resource")
|
|
396
|
+
if resource is None:
|
|
397
|
+
resource = start_resource(loader)
|
|
398
|
+
state["resource"] = resource
|
|
399
|
+
loaded = resource.read()
|
|
400
|
+
children = props.pop("children", None)
|
|
401
|
+
if children:
|
|
402
|
+
return loaded(*children, **props)
|
|
403
|
+
return loaded(**props)
|
|
404
|
+
|
|
405
|
+
Lazy.__name__ = getattr(loader, "__name__", "Lazy")
|
|
406
|
+
if Lazy.__name__ == "<lambda>":
|
|
407
|
+
Lazy.__name__ = "Lazy"
|
|
408
|
+
return Lazy
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
__all__ = [
|
|
412
|
+
"CoroDriver",
|
|
413
|
+
"Resource",
|
|
414
|
+
"Suspend",
|
|
415
|
+
"lazy",
|
|
416
|
+
"start_resource",
|
|
417
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -105,6 +105,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
105
105
|
- **Rich component library:** 25+ built-in components backed by real native widgets: `TextInput`, `Image` / `ImageBackground`, `ScrollView`, `FlatList` / `SectionList`, `Modal`, `Pressable` / `TouchableOpacity`, `Switch` / `Checkbox`, `Slider`, `SegmentedControl`, `Picker`, `DatePicker`, `ProgressBar` / `ActivityIndicator`, `WebView`, and more.
|
|
106
106
|
- **Device APIs:** Cross-platform modules for `Camera`, `Location`, `FileSystem`, `Notifications`, `Clipboard`, `Share`, `Linking`, `Permissions`, `AppState`, `NetInfo`, `SecureStore`, `Battery`, `Haptics` / `Vibration`, and `Biometrics`, plus reactive `use_app_state` and `use_net_info` hooks.
|
|
107
107
|
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect` / `use_layout_effect`, refs and imperative handles with `use_ref` / `use_imperative_handle`, and navigation with `use_navigation`, all through one consistent pattern. Components can return a single element, a list of siblings, or `None`, and `Fragment`, `Portal`, and reactive `Provider` context work the way they do in React.
|
|
108
|
+
- **Async-first rendering:** One `asyncio` event loop runs the whole framework on the platform's main thread. Components can be `async def` and `await` data directly in the body; `Suspense` boundaries show declarative loading states while they wait. `use_resource` fetches during render, `use_effect` accepts coroutine callbacks (cancelled automatically on unmount), `lazy` code-splits components, and `use_transition` / `use_deferred_value` keep the UI responsive during expensive updates.
|
|
108
109
|
- **Developer feedback that finds your bugs:** In dev mode (`pn preview`, hot reload, or `PN_DEV=1`), uncaught errors from renders, effects, and event handlers show a full-screen RedBox with the traceback; unknown style keys and duplicate list keys print "did you mean" warnings; and conditional hooks raise a `HookOrderError` at the source instead of silently cross-wiring state.
|
|
109
110
|
- **Typed `style` prop:** Pass all visual and layout properties through a single `style` dict, fully described by the `pn.Style` `TypedDict` and the ergonomic `pn.style(...)` helper for IDE autocomplete and static checking. Compose reusable styles with `StyleSheet`.
|
|
110
111
|
- **Cross-platform flexbox engine:** A pure-Python, Yoga-style layout engine computes frames once and applies them to native views, so `flex`, `padding`, `aspect_ratio`, and `position: "absolute"` produce the same geometry on Android and iOS.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
pythonnative/__init__.py,sha256=
|
|
1
|
+
pythonnative/__init__.py,sha256=4WYUg8cAV8_JboVBvwc0SR5sRvY4SW7CR4qKrflOBm0,8680
|
|
2
2
|
pythonnative/_ios_log.py,sha256=Oi7V28VxcVoZyrpAirvLeEmUW18McqnU87V4d37Zzlw,2582
|
|
3
3
|
pythonnative/alerts.py,sha256=mIANysFlaHwL5EqKnvNcyiJN9rGiZi9XDrD9Jpz1RFM,9340
|
|
4
|
-
pythonnative/animated.py,sha256=
|
|
4
|
+
pythonnative/animated.py,sha256=OzDqG9ijkxF0rfU0glmgmGikbVB1s5ExoWndy74EhBw,36214
|
|
5
5
|
pythonnative/appearance.py,sha256=vMkDRIz9ptaprMmlPmiYJLVwrDslfnc1bhwyE27M2ZI,3891
|
|
6
|
-
pythonnative/components.py,sha256=
|
|
6
|
+
pythonnative/components.py,sha256=xmJFHw88xPzUQQGeVVhujrIiNLQiQTBZZkPxOD8iWoA,108436
|
|
7
7
|
pythonnative/diagnostics.py,sha256=F6Ym163wSZI93j_ScwRWULIuxRxQ39f5lqvXRDP93po,7078
|
|
8
8
|
pythonnative/element.py,sha256=jbCMPiVq4oRgbSAbKdltBJ1BU6Aom1qKoc6-FsbpJ74,2902
|
|
9
9
|
pythonnative/events.py,sha256=EFYz8HxVOTA3soAe0Z9P2gZN9ShLjXhKH7I34DmnNbM,7895
|
|
10
10
|
pythonnative/gestures.py,sha256=6qe_s3Cv2M5_V-_PgAQ5y9QbYV812B0xVmh8dRDHYMY,29940
|
|
11
|
-
pythonnative/hooks.py,sha256=
|
|
11
|
+
pythonnative/hooks.py,sha256=tpMHxESLndaI_PXHNOcV_4B_u7ZzijQtSXH8_RU5kko,65422
|
|
12
12
|
pythonnative/hot_reload.py,sha256=2HI6u-fc4JRudc35BAe8RXmPIXLWh1imoH4fMVqu_Ks,25502
|
|
13
13
|
pythonnative/images.py,sha256=JFjBJdZzZoQmAOfQ7QHqi7dYJ75XTs3u2lweQHGbdPc,6466
|
|
14
14
|
pythonnative/layout.py,sha256=Ck_FCLSk0g4hdOzHo0c50cbvLe8Q8caQAxywXgnFhKM,49077
|
|
@@ -18,11 +18,12 @@ pythonnative/net.py,sha256=8D7EyEC8-JkYxlrtVfTDApOGzzdcA4P4kaCdYBBZI8E,8082
|
|
|
18
18
|
pythonnative/platform.py,sha256=WtjxR04CqVhSnjJu-3upk8bwXYaSWr2N0MOVoNov-h0,4994
|
|
19
19
|
pythonnative/platform_metrics.py,sha256=1r1kMbgS9LcVTbdpP5qRD4oHEvw70te5BXEyd_16Ihc,8961
|
|
20
20
|
pythonnative/preview.py,sha256=RoZv1s142GWj9xpg9v5Kh-0YuuhpEfwwb_JdnzboRLw,19355
|
|
21
|
-
pythonnative/reconciler.py,sha256=
|
|
22
|
-
pythonnative/runtime.py,sha256=
|
|
23
|
-
pythonnative/screen.py,sha256=
|
|
24
|
-
pythonnative/storage.py,sha256=
|
|
21
|
+
pythonnative/reconciler.py,sha256=6tzn5CsGvGypTnQ7sglEimfrdl7K3dP9W464n9naWZU,96412
|
|
22
|
+
pythonnative/runtime.py,sha256=m4MllJcApoor822LrceIWNLa2YPbd5lgeijQd3yqp0c,25701
|
|
23
|
+
pythonnative/screen.py,sha256=Z0jYl_ZfZkde_Kel2m7kGMNEQesHQudshvI5WmXLvhU,83246
|
|
24
|
+
pythonnative/storage.py,sha256=DNhRJWkT_iwb8lb-8YgTx2P_BHnYkOePLeOPPxcvREc,11983
|
|
25
25
|
pythonnative/style.py,sha256=-XP1vbqiTEkQraFF0JknZLHIzVsou-GUhrKz5zQ7vVk,20030
|
|
26
|
+
pythonnative/suspense.py,sha256=IxWGXFP3Odriy0uQe9zhrNrObdKer0_WT6v9Vi8Vatg,14665
|
|
26
27
|
pythonnative/utils.py,sha256=-hwe_YS19ebpjeygdl3dGeVsYzO4G74rYD53svSi0rI,7593
|
|
27
28
|
pythonnative/virtual_rows.py,sha256=4YK3CeZobW-6F6GCXzGNFfmOaAiigmH4HXtqjYDRbyU,5056
|
|
28
29
|
pythonnative/cli/__init__.py,sha256=NM1psvKe8jT0vzp8Ak4MMoygZz4P_msk5g-YEsY8xLk,232
|
|
@@ -38,15 +39,15 @@ pythonnative/native_modules/haptics.py,sha256=YklgquXUFyQS2a6AizflJj-dM2LHyS86Rd
|
|
|
38
39
|
pythonnative/native_modules/linking.py,sha256=dny2geVaO975JYxoc7DD_PvGs2FyZ1QvZ7JySxbv5dY,5044
|
|
39
40
|
pythonnative/native_modules/location.py,sha256=K_zXUP6XWqJt3qqUMkop_TdMn-yOQB_2TZ18eEQ9Rdc,7606
|
|
40
41
|
pythonnative/native_modules/net_info.py,sha256=QGoi4xZqk9IMyKR9m-delWNRDjEgYRDmxK4lGhZpLoE,5104
|
|
41
|
-
pythonnative/native_modules/notifications.py,sha256=
|
|
42
|
-
pythonnative/native_modules/permissions.py,sha256=
|
|
42
|
+
pythonnative/native_modules/notifications.py,sha256=w2_ChU8Ae-QGnijIxtCOGbBFxR68GlMdDP-LMYmiTyw,8448
|
|
43
|
+
pythonnative/native_modules/permissions.py,sha256=m2kPUiKYZ3ws1bRY4DGgAzmRCzBnu-zQ905z8A6C6UA,7736
|
|
43
44
|
pythonnative/native_modules/secure_store.py,sha256=PsnUwHzUPt-CzCAwM6BhidOBi9yofRD2uNtQIqAGofU,5953
|
|
44
45
|
pythonnative/native_modules/share.py,sha256=B9ovknmnjeQvGQ8MQ14Hmq6E1x-JA469APMburJ0KUg,4727
|
|
45
46
|
pythonnative/native_views/__init__.py,sha256=93v7pt0_7f8BWqOiwSvlq20F1oJLfCnmQBvLlCDA-Xk,15340
|
|
46
|
-
pythonnative/native_views/android.py,sha256=
|
|
47
|
+
pythonnative/native_views/android.py,sha256=vAS1BIZG_gX0nuI4ibEum2XNJc4bv3_uGiHj4dsc_PY,134114
|
|
47
48
|
pythonnative/native_views/base.py,sha256=NFDlEIoKCINzcVkf2uobT9TgujPb1RClLS3TzwH72MY,9668
|
|
48
49
|
pythonnative/native_views/desktop.py,sha256=ZZZ1aw9iRswAfkVoYMUsFz2ztppRgFpMxxwrBkNS7lw,61192
|
|
49
|
-
pythonnative/native_views/ios.py,sha256=
|
|
50
|
+
pythonnative/native_views/ios.py,sha256=1QhEx9FoHkar3DyuItVWGwzO3oEzrW-TGTPyxtsQDtI,179200
|
|
50
51
|
pythonnative/project/__init__.py,sha256=kAhWLONs53H1L7xVtczLmzYcQSOOkdRZTAJnIMhpBts,2064
|
|
51
52
|
pythonnative/project/android.py,sha256=SX0uu4-tz4WdYAf9sbycQPSmoQ8fbtAADeUWk9G8ybM,19127
|
|
52
53
|
pythonnative/project/builder.py,sha256=DokY-YidsDeWYgaixrpD3Kzp7_XudibltI8X_WdBIcE,19096
|
|
@@ -112,9 +113,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
|
|
|
112
113
|
pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
|
|
113
114
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=HLRr3cmouRqQeXG13WrZ2XSAmgMgCsNXfZpbwkxeFcs,1385
|
|
114
115
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
|
|
115
|
-
pythonnative-0.
|
|
116
|
-
pythonnative-0.
|
|
117
|
-
pythonnative-0.
|
|
118
|
-
pythonnative-0.
|
|
119
|
-
pythonnative-0.
|
|
120
|
-
pythonnative-0.
|
|
116
|
+
pythonnative-0.25.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
|
|
117
|
+
pythonnative-0.25.0.dist-info/METADATA,sha256=JiIpbQJggQXhQhjk_ky3Rn-W1EhWc3yP30-6rvqpk2U,11541
|
|
118
|
+
pythonnative-0.25.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
119
|
+
pythonnative-0.25.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
|
|
120
|
+
pythonnative-0.25.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
|
|
121
|
+
pythonnative-0.25.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|