Skip to content

Commit 6b468fd

Browse files
authored
feat(runtime)!: add asyncio loop and awaitable APIs (#5)
1 parent 2488faf commit 6b468fd

36 files changed

Lines changed: 4147 additions & 887 deletions

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,15 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
107107
- `native_modules` – native API modules for device capabilities (`native_modules/`)
108108
- `native_views` – platform-specific native view creation and updates (`native_views/`)
109109
- `navigation` – navigation containers and stack/tab/drawer navigators (`navigation.py`)
110+
- `net` – awaitable HTTP client (`net.py`)
110111
- `package``src/pythonnative/__init__.py` exports and package boundary
111112
- `platform``Platform.OS`/`Platform.select` and version detection (`platform.py`)
112113
- `platform_metrics` – platform-reported metrics like safe-area insets and bar heights (`platform_metrics.py`)
113114
- `reconciler` – virtual view tree diffing and reconciliation (`reconciler.py`)
115+
- `runtime` – framework-wide asyncio loop and thread-safe future helpers (`runtime.py`)
114116
- `screen` – screen host, native lifecycle bridge, and render scheduling (`screen.py`)
115117
- `sdk` – public extension SDK for custom native components (`sdk/`)
118+
- `storage` – AsyncStorage key/value persistence and `use_persisted_state` (`storage.py`)
116119
- `style` – StyleSheet and theming (`style.py`)
117120
- `utils` – shared utilities (`utils.py`)
118121

docs/api/alerts.md

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
# Alerts
22

3-
The [`Alert`][pythonnative.Alert] class provides imperative access to
4-
the host platform's alert dialogs and action sheets. Alerts are *not*
5-
part of the element tree — they're fire-and-forget calls that present
6-
a native dialog and dispatch button callbacks.
3+
The [`Alert`][pythonnative.alerts.Alert] class provides imperative
4+
access to the host platform's alert dialogs and action sheets. Alerts
5+
are *not* part of the element tree.
6+
7+
There are three entry points:
8+
9+
- [`Alert.show`][pythonnative.alerts.Alert.show]: fire-and-forget
10+
one-button notice (no return value).
11+
- [`Alert.confirm`][pythonnative.alerts.Alert.confirm]: awaitable
12+
two-button yes/no, resolves to a ``bool``.
13+
- [`Alert.choose`][pythonnative.alerts.Alert.choose]: awaitable
14+
multi-button picker / action sheet, resolves to the selected
15+
label (or ``None`` if dismissed).
716

817
::: pythonnative.alerts
918
options:
@@ -14,17 +23,20 @@ a native dialog and dispatch button callbacks.
1423

1524
## Patterns
1625

17-
- **Confirm before destructive actions**: pair a `"destructive"`
18-
button with a `"cancel"` button via
19-
[`Alert.confirm`][pythonnative.alerts.Alert.confirm].
20-
- **Action sheets**: pass `style="action_sheet"` to render an iOS-style
21-
bottom sheet; on Android this falls back to a regular dialog.
22-
- **Pickers**: the built-in [`Picker`][pythonnative.Picker] component
23-
is implemented on top of action sheets — use it for select/dropdown
26+
- **Confirm before destructive actions**: ``await pn.Alert.confirm(...)``
27+
inside an `async def`, then branch on the boolean result.
28+
- **Pick from options**: ``await pn.Alert.choose(title, options=[...])``
29+
returns the selected label.
30+
- **Pickers**: the built-in
31+
[`Picker`][pythonnative.components.Picker] component is
32+
implemented on top of action sheets — use it for select/dropdown
2433
widgets.
2534

2635
## Testing
2736

28-
When running off-device (e.g., in unit tests), `Alert.show` records
29-
each call to `Alert._test_log` instead of presenting a dialog. Reset
30-
the log with `Alert._test_log.clear()` between cases.
37+
When running off-device (e.g., in unit tests), the alert dispatch
38+
records each call to `Alert._test_log` instead of presenting a
39+
dialog. Use
40+
[`Alert.set_test_response(*indices)`][pythonnative.alerts.Alert.set_test_response]
41+
to script the user's choices for upcoming ``confirm`` / ``choose``
42+
calls. Reset the log with `Alert._test_log.clear()` between cases.

docs/api/hooks.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ slot across renders.
1212
members_order: source
1313
filters: ["!^_"]
1414

15+
## Async hooks
16+
17+
For coroutines and data-driven UI, PythonNative ships dedicated
18+
async-aware hooks layered on top of `use_state` / `use_effect`:
19+
20+
- [`use_async_effect`][pythonnative.use_async_effect] — async sibling
21+
of `use_effect`; cancels the in-flight coroutine on re-run /
22+
unmount.
23+
- [`use_query`][pythonnative.use_query] — subscribes to an async
24+
fetcher and re-renders on data / error / refetch.
25+
- [`use_mutation`][pythonnative.use_mutation] — wraps an async
26+
mutator with loading / error state and a trigger.
27+
- [`use_persisted_state`][pythonnative.use_persisted_state]
28+
`use_state` backed by
29+
[`AsyncStorage`][pythonnative.AsyncStorage].
30+
31+
See the [Async + data guide](../guides/async.md) for a complete
32+
walkthrough.
33+
1534
## Platform-metric hooks
1635

1736
These hooks subscribe to values published by

docs/api/native_modules.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ local notifications. Each module is implemented twice (once per
66
platform) and dispatches at runtime based on the `IS_ANDROID` and
77
`IS_IOS` flags from `pythonnative.utils`.
88

9-
::: pythonnative.native_modules
10-
options:
11-
show_root_heading: false
12-
show_root_toc_entry: false
13-
members_order: source
14-
filters: ["!^_"]
9+
Apart from `FileSystem`, every public method is a coroutine: ``await
10+
Camera.take_photo()``, ``await Location.get_current()``, and so on.
11+
For the call-site patterns and the runtime they're scheduled on, see
12+
the [Async + data guide](../guides/async.md).
1513

1614
## Camera
1715

docs/api/net.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Network
2+
3+
A small, dependency-free async HTTP client. Use [`fetch`][pythonnative.fetch]
4+
for the common "call a JSON API" path; reach for `httpx` / `aiohttp`
5+
if you need multipart, streaming, or HTTP/2.
6+
7+
::: pythonnative.net
8+
options:
9+
show_root_heading: false
10+
show_root_toc_entry: false
11+
members_order: source
12+
filters: ["!^_"]
13+
14+
## Patterns
15+
16+
- **Inside a component**: pair with
17+
[`use_query`][pythonnative.use_query] for loading/error state and
18+
automatic cancellation on unmount.
19+
- **In an event handler**: wrap an `async def` in
20+
[`pn.run_async`][pythonnative.run_async] so a sync `on_click` can
21+
drive an awaitable request.
22+
- **Mutations**: pair with [`use_mutation`][pythonnative.use_mutation]
23+
to track ``loading`` / ``error`` for POST/PUT/DELETE flows.

docs/api/runtime.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Async runtime
2+
3+
PythonNative runs a single framework-wide ``asyncio`` event loop on a
4+
dedicated daemon thread. Every awaitable surface in the framework —
5+
[`use_async_effect`][pythonnative.hooks.use_async_effect],
6+
[`use_query`][pythonnative.hooks.use_query],
7+
[`use_mutation`][pythonnative.hooks.use_mutation],
8+
[`fetch`][pythonnative.net.fetch],
9+
[`AsyncStorage`][pythonnative.storage.AsyncStorage], the awaitable native
10+
modules ([`Camera`][pythonnative.native_modules.camera.Camera] /
11+
[`Location`][pythonnative.native_modules.location.Location] /
12+
[`Notifications`][pythonnative.native_modules.notifications.Notifications]),
13+
and [`Animated`][pythonnative.animated.Animated] composites — schedules
14+
its work on this loop.
15+
16+
::: pythonnative.runtime
17+
options:
18+
show_root_heading: false
19+
show_root_toc_entry: false
20+
members_order: source
21+
filters: ["!^_"]
22+
23+
## Pattern: bridge a sync handler into async code
24+
25+
```python
26+
import pythonnative as pn
27+
28+
29+
@pn.component
30+
def Toolbar():
31+
async def export():
32+
report = await build_report()
33+
await save_to_disk(report)
34+
35+
return pn.Button("Export", on_click=lambda: pn.run_async(export()))
36+
```
37+
38+
## Next steps
39+
40+
- Walk through the async surface end-to-end:
41+
[Async + data guide](../guides/async.md).

docs/api/storage.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Storage
2+
3+
Key/value persistence backed by the platform-native store
4+
(`NSUserDefaults` on iOS, `SharedPreferences` on Android, a local
5+
JSON file in desktop tests). All operations are coroutines so they
6+
don't block the framework loop.
7+
8+
::: pythonnative.storage
9+
options:
10+
show_root_heading: false
11+
show_root_toc_entry: false
12+
members_order: source
13+
filters: ["!^_"]
14+
15+
## Patterns
16+
17+
- **Token / session storage**: write strings with
18+
[`set`][pythonnative.AsyncStorage] and read with
19+
[`get`][pythonnative.AsyncStorage].
20+
- **Complex values**: use
21+
[`set_json`][pythonnative.AsyncStorage] /
22+
[`get_json`][pythonnative.AsyncStorage] for dicts, lists, and
23+
primitives.
24+
- **Component state that survives restarts**: reach for
25+
[`use_persisted_state`][pythonnative.use_persisted_state] instead
26+
of manually wiring an effect to `AsyncStorage`.
27+
28+
## Next steps
29+
30+
- The pattern is walked through end-to-end in the
31+
[Async + data guide](../guides/async.md).

docs/concepts/components.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,13 @@ pn.Column(
104104

105105
**Imperative APIs:**
106106

107-
- [`Alert.show(title, message, buttons, style)`][pythonnative.Alert]:
108-
present a native alert dialog or action sheet.
109-
- [`Alert.confirm(title, on_confirm, on_cancel)`][pythonnative.alerts.Alert.confirm]:
110-
two-button confirm/cancel.
107+
- [`Alert.show(title, message)`][pythonnative.alerts.Alert.show]:
108+
fire-and-forget single-button notice.
109+
- [`await Alert.confirm(title, message)`][pythonnative.alerts.Alert.confirm]:
110+
awaitable two-button yes/no — resolves to a ``bool``.
111+
- [`await Alert.choose(title, options=[...])`][pythonnative.alerts.Alert.choose]:
112+
awaitable multi-button picker / action sheet — resolves to the
113+
selected label (or ``None``).
111114

112115
**Animations:**
113116

docs/guides/animations.md

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# Animations
22

33
PythonNative ships an `Animated` API modelled on React Native's. It's
4-
designed for the common case where a small set
5-
of style properties (opacity, transform, color) need to interpolate
6-
smoothly over time without re-rendering the component tree on every
7-
frame.
4+
designed for the common case where a small set of style properties
5+
(opacity, transform, color) need to interpolate smoothly over time
6+
without re-rendering the component tree on every frame.
87

98
## Mental model
109

@@ -14,8 +13,10 @@ frame.
1413
2. Bind the value into the `style` of an `Animated.View`,
1514
`Animated.Text`, or `Animated.Image`.
1615
3. Drive the value with `Animated.timing`, `Animated.spring`, or
17-
`Animated.decay`. Each driver returns a handle with `.start()` /
18-
`.stop()`.
16+
`Animated.decay`. Each driver returns a handle with two faces:
17+
- `handle.start()` is fire-and-forget (returns `self`).
18+
- `await handle` runs the animation and suspends until it
19+
completes. Cancelling the awaiting task stops the animation.
1920

2021
The animated component captures a `ref` to the underlying native view
2122
(via the same [`use_ref`][pythonnative.use_ref] mechanism users have
@@ -34,10 +35,10 @@ import pythonnative as pn
3435
def FadeInBox():
3536
opacity = pn.use_animated_value(0.0)
3637

37-
def _fade_in():
38-
pn.Animated.timing(opacity, to=1.0, duration=400).start()
38+
async def _fade_in():
39+
await pn.Animated.timing(opacity, to=1.0, duration=400)
3940

40-
pn.use_effect(_fade_in, [])
41+
pn.use_async_effect(_fade_in, [])
4142

4243
return pn.Animated.View(
4344
pn.Text("Hello!"),
@@ -51,7 +52,17 @@ def FadeInBox():
5152
```
5253

5354
`opacity` starts at `0.0` and the timing animation interpolates it to
54-
`1.0` over 400 ms.
55+
`1.0` over 400 ms. Using `use_async_effect` means the in-flight
56+
animation is automatically cancelled if the component unmounts before
57+
the 400 ms is up.
58+
59+
If you don't need to react to completion, the synchronous form is fine
60+
too:
61+
62+
```python
63+
def _press():
64+
pn.Animated.timing(opacity, to=1.0, duration=400).start()
65+
```
5566

5667
## Spring animation on press
5768

@@ -80,17 +91,21 @@ animation property.
8091
## Sequencing and parallel composition
8192

8293
```python
83-
opacity = pn.use_animated_value(0.0)
84-
translate_y = pn.use_animated_value(20.0)
85-
86-
pn.Animated.parallel([
87-
pn.Animated.timing(opacity, to=1.0, duration=300),
88-
pn.Animated.spring(translate_y, to=0.0),
89-
]).start()
94+
async def _intro():
95+
opacity = pn.use_animated_value(0.0)
96+
translate_y = pn.use_animated_value(20.0)
97+
98+
await pn.Animated.parallel([
99+
pn.Animated.timing(opacity, to=1.0, duration=300),
100+
pn.Animated.spring(translate_y, to=0.0),
101+
])
102+
await pn.Animated.delay(80)
103+
await pn.Animated.timing(opacity, to=0.5, duration=200)
90104
```
91105

92-
Use `Animated.sequence` for one-after-another execution and
93-
`Animated.delay(ms)` to insert pauses inside a sequence.
106+
`Animated.parallel` returns when **all** animations finish.
107+
`Animated.sequence` runs animations one-after-another. Both are also
108+
awaitable.
94109

95110
## Easing
96111

@@ -101,7 +116,18 @@ Use `Animated.sequence` for one-after-another execution and
101116

102117
`start()` returns the handle you started with, and the handle exposes
103118
`.stop()`. A common pattern is to keep the handle in a `use_ref` so
104-
you can cancel a long-running animation when the user interrupts.
119+
you can cancel a long-running animation when the user interrupts. If
120+
you're awaiting the animation instead, cancelling the awaiting task
121+
stops the animation:
122+
123+
```python
124+
async def _enter():
125+
await pn.Animated.timing(opacity, to=1.0, duration=2000)
126+
127+
task = pn.run_async(_enter())
128+
# Sometime later:
129+
task.cancel() # animation snaps to wherever it was; opacity stops here.
130+
```
105131

106132
## When NOT to use `Animated`
107133

0 commit comments

Comments
 (0)