pythonnative 0.16.0__tar.gz → 0.17.1__tar.gz

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.
Files changed (124) hide show
  1. {pythonnative-0.16.0/src/pythonnative.egg-info → pythonnative-0.17.1}/PKG-INFO +3 -1
  2. {pythonnative-0.16.0 → pythonnative-0.17.1}/pyproject.toml +8 -1
  3. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/__init__.py +26 -2
  4. pythonnative-0.17.1/src/pythonnative/alerts.py +298 -0
  5. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/animated.py +224 -180
  6. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/hooks.py +271 -1
  7. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/layout.py +35 -1
  8. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/camera.py +118 -121
  9. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/file_system.py +3 -3
  10. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/location.py +90 -109
  11. pythonnative-0.17.1/src/pythonnative/native_modules/notifications.py +206 -0
  12. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_views/android.py +187 -66
  13. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_views/ios.py +133 -42
  14. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/navigation.py +57 -2
  15. pythonnative-0.17.1/src/pythonnative/net.py +244 -0
  16. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/reconciler.py +38 -0
  17. pythonnative-0.17.1/src/pythonnative/runtime.py +487 -0
  18. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/screen.py +52 -3
  19. pythonnative-0.17.1/src/pythonnative/storage.py +409 -0
  20. {pythonnative-0.16.0 → pythonnative-0.17.1/src/pythonnative.egg-info}/PKG-INFO +3 -1
  21. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/SOURCES.txt +7 -0
  22. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/requires.txt +2 -0
  23. pythonnative-0.17.1/tests/test_alert.py +115 -0
  24. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_animated.py +68 -45
  25. pythonnative-0.17.1/tests/test_async_hooks.py +339 -0
  26. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_layout.py +80 -0
  27. pythonnative-0.17.1/tests/test_net.py +189 -0
  28. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_reconciler.py +39 -0
  29. pythonnative-0.17.1/tests/test_runtime.py +146 -0
  30. pythonnative-0.17.1/tests/test_storage.py +215 -0
  31. pythonnative-0.16.0/src/pythonnative/alerts.py +0 -112
  32. pythonnative-0.16.0/src/pythonnative/native_modules/notifications.py +0 -184
  33. pythonnative-0.16.0/tests/test_alert.py +0 -68
  34. {pythonnative-0.16.0 → pythonnative-0.17.1}/LICENSE +0 -0
  35. {pythonnative-0.16.0 → pythonnative-0.17.1}/README.md +0 -0
  36. {pythonnative-0.16.0 → pythonnative-0.17.1}/setup.cfg +0 -0
  37. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/_ios_log.py +0 -0
  38. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/cli/__init__.py +0 -0
  39. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/cli/pn.py +0 -0
  40. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/components.py +0 -0
  41. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/element.py +0 -0
  42. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/hot_reload.py +0 -0
  43. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_modules/__init__.py +0 -0
  44. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_views/__init__.py +0 -0
  45. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/native_views/base.py +0 -0
  46. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/platform.py +0 -0
  47. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/platform_metrics.py +0 -0
  48. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/sdk/__init__.py +0 -0
  49. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/sdk/_components.py +0 -0
  50. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/style.py +0 -0
  51. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
  52. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  53. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
  54. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  55. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
  56. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
  57. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
  58. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
  59. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  60. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
  61. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  62. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
  63. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
  64. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  65. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  66. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  67. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  68. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  69. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  70. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  71. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  72. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  73. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  74. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  75. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  76. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  77. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  78. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  79. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  80. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  81. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
  82. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/build.gradle +0 -0
  83. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  84. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  85. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  86. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradlew +0 -0
  87. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  88. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  89. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  90. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  91. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  92. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  93. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  94. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  95. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  96. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  97. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
  98. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  99. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  100. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  101. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  102. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  103. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative/utils.py +0 -0
  104. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  105. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/entry_points.txt +0 -0
  106. {pythonnative-0.16.0 → pythonnative-0.17.1}/src/pythonnative.egg-info/top_level.txt +0 -0
  107. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_cli.py +0 -0
  108. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_components.py +0 -0
  109. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_element.py +0 -0
  110. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_hooks.py +0 -0
  111. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_hot_reload.py +0 -0
  112. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_ios_log.py +0 -0
  113. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_metric_hooks.py +0 -0
  114. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_native_views.py +0 -0
  115. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_navigation.py +0 -0
  116. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_new_components.py +0 -0
  117. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_platform.py +0 -0
  118. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_platform_metrics.py +0 -0
  119. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_ref.py +0 -0
  120. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_screen.py +0 -0
  121. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_sdk.py +0 -0
  122. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_smoke.py +0 -0
  123. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_style.py +0 -0
  124. {pythonnative-0.16.0 → pythonnative-0.17.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.16.0
3
+ Version: 0.17.1
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -55,11 +55,13 @@ Requires-Dist: black>=24.0; extra == "dev"
55
55
  Requires-Dist: ruff>=0.5; extra == "dev"
56
56
  Requires-Dist: mypy>=1.10; extra == "dev"
57
57
  Requires-Dist: pytest>=8.0; extra == "dev"
58
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
58
59
  Provides-Extra: ci
59
60
  Requires-Dist: black>=24.0; extra == "ci"
60
61
  Requires-Dist: ruff>=0.5; extra == "ci"
61
62
  Requires-Dist: mypy>=1.10; extra == "ci"
62
63
  Requires-Dist: pytest>=8.0; extra == "ci"
64
+ Requires-Dist: pytest-asyncio>=0.23; extra == "ci"
63
65
  Dynamic: license-file
64
66
 
65
67
  <p align="center">
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.16.0"
7
+ version = "0.17.1"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
@@ -43,12 +43,14 @@ dev = [
43
43
  "ruff>=0.5",
44
44
  "mypy>=1.10",
45
45
  "pytest>=8.0",
46
+ "pytest-asyncio>=0.23",
46
47
  ]
47
48
  ci = [
48
49
  "black>=24.0",
49
50
  "ruff>=0.5",
50
51
  "mypy>=1.10",
51
52
  "pytest>=8.0",
53
+ "pytest-asyncio>=0.23",
52
54
  ]
53
55
 
54
56
  [project.scripts]
@@ -66,6 +68,8 @@ Documentation = "https://docs.pythonnative.com/"
66
68
 
67
69
 
68
70
 
71
+
72
+
69
73
  [tool.setuptools.packages.find]
70
74
  where = ["src"]
71
75
 
@@ -120,6 +124,9 @@ convention = "google"
120
124
  line-length = 120
121
125
  target-version = ['py39']
122
126
 
127
+ [tool.pytest.ini_options]
128
+ asyncio_mode = "auto"
129
+
123
130
  # ── Semantic Release ────────────────────────────────────────────────
124
131
 
125
132
  [tool.semantic_release]
@@ -51,9 +51,9 @@ Example:
51
51
  ```
52
52
  """
53
53
 
54
- __version__ = "0.16.0"
54
+ __version__ = "0.17.1"
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
@@ -0,0 +1,298 @@
1
+ """Imperative, awaitable system alerts.
2
+
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).
14
+
15
+ Example:
16
+ ```python
17
+ import pythonnative as pn
18
+
19
+
20
+ async def maybe_delete():
21
+ if await pn.Alert.confirm(
22
+ title="Delete item?",
23
+ message="This action cannot be undone.",
24
+ confirm_label="Delete",
25
+ cancel_label="Keep",
26
+ ):
27
+ await delete_item()
28
+ ```
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import asyncio
34
+ from typing import Any, Dict, List, Optional, Sequence
35
+
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
+ # ======================================================================
109
+
110
+
111
+ class Alert:
112
+ """Imperative alert / action-sheet helper.
113
+
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.
120
+ """
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
+
153
+ @staticmethod
154
+ def show(
155
+ title: str,
156
+ message: Optional[str] = None,
157
+ *,
158
+ button: str = "OK",
159
+ ) -> None:
160
+ """Display a simple, one-button alert and return immediately.
161
+
162
+ Args:
163
+ title: Dialog title.
164
+ message: Optional body text.
165
+ button: Label for the single dismiss button (default
166
+ ``"OK"``).
167
+
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,
179
+ )
180
+
181
+ @staticmethod
182
+ async def confirm(
183
+ title: str,
184
+ message: Optional[str] = None,
185
+ *,
186
+ confirm_label: str = "OK",
187
+ cancel_label: str = "Cancel",
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(
216
+ title=title,
217
+ message=message,
218
+ buttons=[
219
+ {"label": cancel_label, "style": "cancel"},
220
+ {"label": confirm_label, "style": "default"},
221
+ ],
222
+ style="alert",
223
+ on_result=_on_result,
224
+ )
225
+ return await future
226
+
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"]