pythonnative 0.16.0__py3-none-any.whl → 0.17.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 CHANGED
@@ -51,9 +51,9 @@ Example:
51
51
  ```
52
52
  """
53
53
 
54
- __version__ = "0.16.0"
54
+ __version__ = "0.17.0"
55
55
 
56
- from . import sdk
56
+ from . import runtime, sdk
57
57
  from .alerts import Alert
58
58
  from .animated import Animated, AnimatedValue, use_animated_value
59
59
  from .components import (
@@ -103,17 +103,23 @@ from .components import (
103
103
  )
104
104
  from .element import Element
105
105
  from .hooks import (
106
+ MutationCall,
107
+ MutationState,
106
108
  Provider,
109
+ QueryResult,
107
110
  batch_updates,
108
111
  component,
109
112
  create_context,
110
113
  memo,
114
+ use_async_effect,
111
115
  use_callback,
112
116
  use_context,
113
117
  use_effect,
114
118
  use_keyboard_height,
115
119
  use_memo,
120
+ use_mutation,
116
121
  use_navigation,
122
+ use_query,
117
123
  use_reducer,
118
124
  use_ref,
119
125
  use_safe_area_insets,
@@ -129,7 +135,9 @@ from .navigation import (
129
135
  use_focus_effect,
130
136
  use_route,
131
137
  )
138
+ from .net import HTTPError, Response, fetch
132
139
  from .platform import Platform
140
+ from .runtime import run_async
133
141
  from .screen import create_screen
134
142
  from .sdk import (
135
143
  Props,
@@ -138,6 +146,7 @@ from .sdk import (
138
146
  native_component,
139
147
  register_component,
140
148
  )
149
+ from .storage import AsyncStorage, use_persisted_state
141
150
  from .style import (
142
151
  AlignItems,
143
152
  AlignSelf,
@@ -219,13 +228,20 @@ __all__ = [
219
228
  "component",
220
229
  "create_context",
221
230
  "memo",
231
+ "MutationCall",
232
+ "MutationState",
233
+ "QueryResult",
234
+ "use_async_effect",
222
235
  "use_callback",
223
236
  "use_context",
224
237
  "use_effect",
225
238
  "use_focus_effect",
226
239
  "use_keyboard_height",
227
240
  "use_memo",
241
+ "use_mutation",
228
242
  "use_navigation",
243
+ "use_persisted_state",
244
+ "use_query",
229
245
  "use_reducer",
230
246
  "use_ref",
231
247
  "use_route",
@@ -274,6 +290,14 @@ __all__ = [
274
290
  "FileSystem",
275
291
  "Location",
276
292
  "Notifications",
293
+ # Networking + persistence
294
+ "AsyncStorage",
295
+ "fetch",
296
+ "HTTPError",
297
+ "Response",
298
+ # Runtime
299
+ "run_async",
300
+ "runtime",
277
301
  # Platform
278
302
  "Platform",
279
303
  # Custom-component SDK
pythonnative/alerts.py CHANGED
@@ -1,112 +1,298 @@
1
- """Imperative system alerts.
1
+ """Imperative, awaitable system alerts.
2
2
 
3
- Modeled on React Native's `Alert.alert()`. Alerts are not part of
4
- the element tree they're imperative, fire-and-forget calls that
5
- trigger a native dialog.
3
+ Inspired by React Native's ``Alert.alert()`` but designed around
4
+ ``async`` / ``await`` instead of per-button callbacks. There are three
5
+ entry points:
6
+
7
+ - [`Alert.show`][pythonnative.alerts.Alert.show]: fire-and-forget
8
+ one-button notice (no return value).
9
+ - [`Alert.confirm`][pythonnative.alerts.Alert.confirm]: awaitable
10
+ two-button yes/no, resolves to a ``bool``.
11
+ - [`Alert.choose`][pythonnative.alerts.Alert.choose]: awaitable
12
+ multi-button picker / action sheet, resolves to the selected
13
+ label (or ``None`` if dismissed).
6
14
 
7
15
  Example:
8
16
  ```python
9
17
  import pythonnative as pn
10
18
 
11
- def confirm_delete():
12
- pn.Alert.show(
19
+
20
+ async def maybe_delete():
21
+ if await pn.Alert.confirm(
13
22
  title="Delete item?",
14
23
  message="This action cannot be undone.",
15
- buttons=[
16
- {"label": "Cancel", "style": "cancel"},
17
- {
18
- "label": "Delete",
19
- "style": "destructive",
20
- "on_press": delete_item,
21
- },
22
- ],
23
- )
24
+ confirm_label="Delete",
25
+ cancel_label="Keep",
26
+ ):
27
+ await delete_item()
24
28
  ```
25
29
  """
26
30
 
27
31
  from __future__ import annotations
28
32
 
29
- from typing import Any, Callable, Dict, List, Optional
33
+ import asyncio
34
+ from typing import Any, Dict, List, Optional, Sequence
30
35
 
31
36
  from .platform import Platform
37
+ from .runtime import resolve_future
38
+
39
+ # ======================================================================
40
+ # Internal dispatch helpers
41
+ # ======================================================================
42
+
43
+
44
+ def _dispatch_alert(
45
+ *,
46
+ title: str,
47
+ message: Optional[str],
48
+ buttons: List[Dict[str, Any]],
49
+ style: str,
50
+ on_result: Any,
51
+ ) -> None:
52
+ """Route an alert request to the active platform presenter.
53
+
54
+ ``buttons`` is a list of ``{"label": str, "style":
55
+ "default"|"cancel"|"destructive"}`` dicts. The presenter must
56
+ invoke ``on_result(index)`` exactly once when the user picks a
57
+ button, or ``on_result(-1)`` if the dialog is dismissed without a
58
+ selection. ``on_result`` may run on any thread.
59
+ """
60
+ if Platform.is_ios:
61
+ try:
62
+ from .native_views.ios import _present_alert as _ios_present_alert
63
+
64
+ _ios_present_alert(
65
+ title=title,
66
+ message=message,
67
+ buttons=buttons,
68
+ style=style,
69
+ on_result=on_result,
70
+ )
71
+ return
72
+ except Exception:
73
+ on_result(-1)
74
+ return
75
+
76
+ if Platform.is_android:
77
+ try:
78
+ from .native_views.android import _present_alert as _android_present_alert
79
+
80
+ _android_present_alert(
81
+ title=title,
82
+ message=message,
83
+ buttons=buttons,
84
+ style=style,
85
+ on_result=on_result,
86
+ )
87
+ return
88
+ except Exception:
89
+ on_result(-1)
90
+ return
91
+
92
+ # Test backend: record the call so unit tests can assert on it,
93
+ # then deliver the configured response.
94
+ Alert._test_log.append(
95
+ {
96
+ "title": title,
97
+ "message": message,
98
+ "buttons": list(buttons),
99
+ "style": style,
100
+ }
101
+ )
102
+ response = Alert._next_test_response()
103
+ on_result(response)
104
+
105
+
106
+ # ======================================================================
107
+ # Public Alert API
108
+ # ======================================================================
32
109
 
33
110
 
34
111
  class Alert:
35
112
  """Imperative alert / action-sheet helper.
36
113
 
37
- All methods are static. Use `Alert.show()` for an alert dialog
38
- and pass ``style="action_sheet"`` for an iOS-style action sheet.
114
+ All methods are static. Use [`show`][pythonnative.alerts.Alert.show]
115
+ for a fire-and-forget single-button notice,
116
+ [`confirm`][pythonnative.alerts.Alert.confirm] for an awaitable
117
+ yes/no dialog, and
118
+ [`choose`][pythonnative.alerts.Alert.choose] for a multi-option
119
+ picker.
39
120
  """
40
121
 
122
+ #: Records every alert call when running off-device. Tests reset
123
+ #: this between cases via ``Alert._test_log.clear()``. Each entry
124
+ #: contains ``title``, ``message``, ``buttons``, and ``style``.
125
+ _test_log: List[Dict[str, Any]] = []
126
+
127
+ #: Queue of indices to deliver to upcoming alerts in tests. Set via
128
+ #: [`Alert.set_test_response`][pythonnative.alerts.Alert.set_test_response].
129
+ #: A negative value (or empty queue) simulates a dismiss.
130
+ _test_responses: List[int] = []
131
+
132
+ @staticmethod
133
+ def set_test_response(*indices: int) -> None:
134
+ """Queue indices to return from upcoming test alerts.
135
+
136
+ Use in async tests to script the user's choices: each pending
137
+ call to [`confirm`][pythonnative.alerts.Alert.confirm] or
138
+ [`choose`][pythonnative.alerts.Alert.choose] pops the next
139
+ queued index. Pass ``-1`` to simulate a dismiss.
140
+
141
+ Args:
142
+ *indices: Sequence of button indices to deliver, oldest
143
+ first. Calls beyond the queue length resolve to ``-1``.
144
+ """
145
+ Alert._test_responses[:] = list(indices)
146
+
147
+ @staticmethod
148
+ def _next_test_response() -> int:
149
+ if Alert._test_responses:
150
+ return Alert._test_responses.pop(0)
151
+ return -1
152
+
41
153
  @staticmethod
42
154
  def show(
43
- *,
44
155
  title: str,
45
156
  message: Optional[str] = None,
46
- buttons: Optional[List[Dict[str, Any]]] = None,
47
- style: str = "alert",
157
+ *,
158
+ button: str = "OK",
48
159
  ) -> None:
49
- """Present a native alert dialog or action sheet.
160
+ """Display a simple, one-button alert and return immediately.
50
161
 
51
162
  Args:
52
- title: Dialog title (required).
163
+ title: Dialog title.
53
164
  message: Optional body text.
54
- buttons: Each button is ``{"label": str, "style":
55
- "default"|"cancel"|"destructive", "on_press": callable}``.
56
- Defaults to a single "OK" button.
57
- style: ``"alert"`` (default) or ``"action_sheet"``.
58
-
59
- On iOS this uses ``UIAlertController``; on Android it uses
60
- ``AlertDialog.Builder``. On the test backend the call is a
61
- no-op so unit tests don't need to mock UIKit/AndroidX.
62
- """
63
- if Platform.is_ios:
64
- try:
65
- from .native_views.ios import _present_alert as _ios_present_alert
165
+ button: Label for the single dismiss button (default
166
+ ``"OK"``).
66
167
 
67
- _ios_present_alert(title=title, message=message, buttons=buttons or [], style=style)
68
- except Exception:
69
- pass
70
- return
71
- if Platform.is_android:
72
- try:
73
- from .native_views.android import _present_alert as _android_present_alert
74
-
75
- _android_present_alert(title=title, message=message, buttons=buttons or [], style=style)
76
- except Exception:
77
- pass
78
- return
79
- # Test environment: record the call so unit tests can assert on it.
80
- Alert._test_log.append(
81
- {
82
- "title": title,
83
- "message": message,
84
- "buttons": list(buttons or []),
85
- "style": style,
86
- }
168
+ This is fire-and-forget. To know what the user did, use
169
+ [`confirm`][pythonnative.alerts.Alert.confirm] or
170
+ [`choose`][pythonnative.alerts.Alert.choose] and ``await``
171
+ the result.
172
+ """
173
+ _dispatch_alert(
174
+ title=title,
175
+ message=message,
176
+ buttons=[{"label": button, "style": "default"}],
177
+ style="alert",
178
+ on_result=lambda _idx: None,
87
179
  )
88
180
 
89
181
  @staticmethod
90
- def confirm(
91
- *,
182
+ async def confirm(
92
183
  title: str,
93
184
  message: Optional[str] = None,
185
+ *,
94
186
  confirm_label: str = "OK",
95
187
  cancel_label: str = "Cancel",
96
- on_confirm: Optional[Callable[[], None]] = None,
97
- on_cancel: Optional[Callable[[], None]] = None,
98
- ) -> None:
99
- """Convenience wrapper for two-button confirm/cancel dialogs."""
100
- Alert.show(
188
+ ) -> bool:
189
+ """Present a two-button yes/no dialog and wait for the choice.
190
+
191
+ Args:
192
+ title: Dialog title.
193
+ message: Optional body text.
194
+ confirm_label: Label for the "yes" button (default
195
+ ``"OK"``).
196
+ cancel_label: Label for the "no" button (default
197
+ ``"Cancel"``).
198
+
199
+ Returns:
200
+ ``True`` if the user pressed the confirm button, ``False``
201
+ for the cancel button or a dismiss.
202
+
203
+ Example:
204
+ ```python
205
+ if await pn.Alert.confirm("Save changes?"):
206
+ await save()
207
+ ```
208
+ """
209
+ loop = asyncio.get_running_loop()
210
+ future: asyncio.Future[bool] = loop.create_future()
211
+
212
+ def _on_result(index: int) -> None:
213
+ resolve_future(future, index == 1)
214
+
215
+ _dispatch_alert(
101
216
  title=title,
102
217
  message=message,
103
218
  buttons=[
104
- {"label": cancel_label, "style": "cancel", "on_press": on_cancel},
105
- {"label": confirm_label, "style": "default", "on_press": on_confirm},
219
+ {"label": cancel_label, "style": "cancel"},
220
+ {"label": confirm_label, "style": "default"},
106
221
  ],
222
+ style="alert",
223
+ on_result=_on_result,
107
224
  )
225
+ return await future
108
226
 
109
- #: Records every Alert.show call when running off-device. Tests
110
- #: should reset this list between cases via
111
- #: ``Alert._test_log.clear()``.
112
- _test_log: List[Dict[str, Any]] = []
227
+ @staticmethod
228
+ async def choose(
229
+ title: str,
230
+ options: Sequence[str],
231
+ *,
232
+ message: Optional[str] = None,
233
+ cancel_label: Optional[str] = None,
234
+ style: str = "action_sheet",
235
+ destructive_labels: Sequence[str] = (),
236
+ ) -> Optional[str]:
237
+ """Present a multi-option picker and wait for the user's choice.
238
+
239
+ Args:
240
+ title: Dialog title.
241
+ options: Sequence of option labels (in display order).
242
+ message: Optional body text.
243
+ cancel_label: If provided, adds a "cancel" button with
244
+ this label. Selecting it resolves to ``None``.
245
+ style: ``"action_sheet"`` (default) for an iOS-style
246
+ sheet, or ``"alert"`` for a stacked alert dialog.
247
+ destructive_labels: Labels in ``options`` that should be
248
+ styled destructively (red on iOS).
249
+
250
+ Returns:
251
+ The selected label, or ``None`` if the user dismissed or
252
+ tapped the cancel button.
253
+
254
+ Example:
255
+ ```python
256
+ choice = await pn.Alert.choose(
257
+ "Photo source",
258
+ options=["Camera", "Gallery"],
259
+ cancel_label="Cancel",
260
+ )
261
+ if choice == "Camera":
262
+ ...
263
+ ```
264
+ """
265
+ if not options:
266
+ raise ValueError("Alert.choose requires at least one option")
267
+
268
+ loop = asyncio.get_running_loop()
269
+ future: asyncio.Future[Optional[str]] = loop.create_future()
270
+
271
+ destructive = set(destructive_labels)
272
+ buttons: List[Dict[str, Any]] = [
273
+ {
274
+ "label": opt,
275
+ "style": "destructive" if opt in destructive else "default",
276
+ }
277
+ for opt in options
278
+ ]
279
+ if cancel_label is not None:
280
+ buttons.append({"label": cancel_label, "style": "cancel"})
281
+
282
+ def _on_result(index: int) -> None:
283
+ if 0 <= index < len(options):
284
+ resolve_future(future, options[index])
285
+ else:
286
+ resolve_future(future, None)
287
+
288
+ _dispatch_alert(
289
+ title=title,
290
+ message=message,
291
+ buttons=buttons,
292
+ style=style,
293
+ on_result=_on_result,
294
+ )
295
+ return await future
296
+
297
+
298
+ __all__ = ["Alert"]