pythonnative 0.24.0__py3-none-any.whl → 0.26.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/cli/pn.py +172 -12
- pythonnative/components.py +59 -0
- pythonnative/diagnostics.py +27 -0
- pythonnative/hooks.py +364 -81
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +2 -1
- pythonnative/native_modules/notifications.py +132 -9
- pythonnative/native_modules/permissions.py +27 -3
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +149 -124
- pythonnative/native_views/desktop.py +50 -49
- pythonnative/native_views/ios.py +240 -161
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- 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/templates/android_template/app/build.gradle +5 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +1 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +88 -12
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +0 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/METADATA +2 -1
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/RECORD +51 -48
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/WHEEL +1 -1
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.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,39 +1,115 @@
|
|
|
1
1
|
package com.pythonnative.android_template
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.Typeface
|
|
4
6
|
import android.os.Bundle
|
|
5
7
|
import android.util.Log
|
|
8
|
+
import android.util.TypedValue
|
|
9
|
+
import android.widget.ScrollView
|
|
6
10
|
import android.widget.TextView
|
|
11
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
7
12
|
import com.chaquo.python.Python
|
|
8
13
|
import com.chaquo.python.android.AndroidPlatform
|
|
9
14
|
|
|
10
15
|
class MainActivity : AppCompatActivity() {
|
|
11
16
|
private val TAG = javaClass.simpleName
|
|
17
|
+
private var pythonReady = false
|
|
12
18
|
|
|
13
19
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
14
20
|
super.onCreate(savedInstanceState)
|
|
15
21
|
Log.d(TAG, "onCreate() called")
|
|
16
22
|
|
|
17
|
-
// Initialize Chaquopy
|
|
18
23
|
if (!Python.isStarted()) {
|
|
19
24
|
Python.start(AndroidPlatform(this))
|
|
20
25
|
}
|
|
21
26
|
try {
|
|
22
|
-
// Set content view to the NavHost layout; the initial screen
|
|
27
|
+
// Set content view to the NavHost layout; the initial screen
|
|
28
|
+
// loads via nav_graph startDestination.
|
|
23
29
|
setContentView(R.layout.activity_main)
|
|
24
|
-
// Optionally, bootstrap Python so first fragment can create the initial screen onCreate
|
|
25
30
|
val py = Python.getInstance()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (BuildConfig.DEBUG) {
|
|
32
|
+
// Dev-only writable source overlay for hot reload.
|
|
33
|
+
py.getModule("pythonnative.hot_reload").callAttr(
|
|
34
|
+
"configure_dev_environment",
|
|
35
|
+
filesDir.absolutePath
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
// Import the entry module now so a broken app fails here,
|
|
39
|
+
// with a full traceback, instead of inside the first fragment.
|
|
31
40
|
py.getModule("app.main")
|
|
41
|
+
pythonReady = true
|
|
32
42
|
} catch (e: Exception) {
|
|
33
43
|
Log.e("PythonNative", "Bootstrap failed", e)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
showBootstrapError(e)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A cold start from a deep link carries the URL on the launch intent.
|
|
49
|
+
intent?.dataString?.let { dispatchUrl(it) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onNewIntent(intent: Intent) {
|
|
53
|
+
super.onNewIntent(intent)
|
|
54
|
+
setIntent(intent)
|
|
55
|
+
intent.dataString?.let { dispatchUrl(it) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - AppState forwarding
|
|
59
|
+
|
|
60
|
+
override fun onResume() {
|
|
61
|
+
super.onResume()
|
|
62
|
+
dispatchAppState("active")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onPause() {
|
|
66
|
+
super.onPause()
|
|
67
|
+
dispatchAppState("inactive")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun onStop() {
|
|
71
|
+
super.onStop()
|
|
72
|
+
dispatchAppState("background")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun dispatchAppState(state: String) {
|
|
76
|
+
if (!pythonReady) return
|
|
77
|
+
try {
|
|
78
|
+
Python.getInstance()
|
|
79
|
+
.getModule("pythonnative.native_modules.app_state")
|
|
80
|
+
.callAttr("dispatch_app_state", state)
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
Log.e("PythonNative", "dispatch_app_state($state) failed", e)
|
|
37
83
|
}
|
|
38
84
|
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Deep links
|
|
87
|
+
|
|
88
|
+
private fun dispatchUrl(url: String) {
|
|
89
|
+
if (!pythonReady) return
|
|
90
|
+
try {
|
|
91
|
+
Python.getInstance()
|
|
92
|
+
.getModule("pythonnative.native_modules.linking")
|
|
93
|
+
.callAttr("dispatch_url", url)
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
Log.e("PythonNative", "dispatch_url failed", e)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// MARK: - Bootstrap error UI
|
|
100
|
+
|
|
101
|
+
private fun showBootstrapError(error: Exception) {
|
|
102
|
+
val text = TextView(this)
|
|
103
|
+
text.text = "PythonNative could not start\n\n" + Log.getStackTraceString(error)
|
|
104
|
+
text.setTextColor(Color.WHITE)
|
|
105
|
+
text.setBackgroundColor(Color.rgb(191, 26, 26))
|
|
106
|
+
text.typeface = Typeface.MONOSPACE
|
|
107
|
+
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
|
108
|
+
val pad = (16 * resources.displayMetrics.density).toInt()
|
|
109
|
+
text.setPadding(pad, pad * 3, pad, pad * 2)
|
|
110
|
+
val scroll = ScrollView(this)
|
|
111
|
+
scroll.setBackgroundColor(Color.rgb(191, 26, 26))
|
|
112
|
+
scroll.addView(text)
|
|
113
|
+
setContentView(scroll)
|
|
114
|
+
}
|
|
39
115
|
}
|