pythonnative 0.36.0__py3-none-any.whl → 0.37.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.
Files changed (174) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/components/layout.py +65 -3
  11. pythonnative/gestures.py +13 -10
  12. pythonnative/hooks.py +41 -26
  13. pythonnative/hosts/__init__.py +13 -36
  14. pythonnative/hosts/native.py +348 -0
  15. pythonnative/images.py +6 -5
  16. pythonnative/native_modules/__init__.py +30 -29
  17. pythonnative/native_modules/app_state.py +10 -6
  18. pythonnative/native_modules/battery.py +20 -84
  19. pythonnative/native_modules/biometrics.py +11 -124
  20. pythonnative/native_modules/camera.py +15 -291
  21. pythonnative/native_modules/clipboard.py +14 -109
  22. pythonnative/native_modules/desktop.py +322 -0
  23. pythonnative/native_modules/file_system.py +15 -21
  24. pythonnative/native_modules/haptics.py +19 -129
  25. pythonnative/native_modules/linking.py +24 -137
  26. pythonnative/native_modules/location.py +28 -180
  27. pythonnative/native_modules/net_info.py +25 -135
  28. pythonnative/native_modules/notifications.py +36 -257
  29. pythonnative/native_modules/permissions.py +20 -184
  30. pythonnative/native_modules/registry.py +513 -0
  31. pythonnative/native_modules/secure_store.py +18 -155
  32. pythonnative/native_modules/share.py +8 -113
  33. pythonnative/native_views/__init__.py +67 -86
  34. pythonnative/native_views/bridge_backend.py +264 -0
  35. pythonnative/platform.py +19 -15
  36. pythonnative/platform_metrics.py +3 -3
  37. pythonnative/project/android.py +0 -3
  38. pythonnative/project/builder.py +38 -16
  39. pythonnative/project/config.py +12 -2
  40. pythonnative/project/plugins.py +366 -0
  41. pythonnative/py.typed +0 -0
  42. pythonnative/runtime.py +35 -203
  43. pythonnative/sdk/__init__.py +48 -33
  44. pythonnative/sdk/_components.py +57 -76
  45. pythonnative/storage.py +18 -243
  46. pythonnative/templates/android_template/app/build.gradle +6 -9
  47. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  48. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  49. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  50. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  51. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  100. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  101. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  104. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  105. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  106. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  110. pythonnative/templates/android_template/settings.gradle +1 -0
  111. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  112. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  155. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  156. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  157. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  158. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  159. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  160. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  161. pythonnative/utils.py +4 -82
  162. pythonnative/virtual_rows.py +3 -3
  163. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  164. pythonnative-0.37.0.dist-info/RECORD +263 -0
  165. pythonnative/hosts/android.py +0 -269
  166. pythonnative/hosts/ios.py +0 -404
  167. pythonnative/native_views/android.py +0 -3702
  168. pythonnative/native_views/ios.py +0 -5253
  169. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  170. pythonnative-0.36.0.dist-info/RECORD +0 -155
  171. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  172. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  173. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  174. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
pythonnative/__init__.py CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  PythonNative is a cross-platform toolkit that turns Python ``@component``
4
4
  functions into real, native Android and iOS views. The component model
5
- is React-like (function components plus hooks), but rendering happens
6
- through direct platform bindings: Chaquopy on Android (Java) and
7
- rubicon-objc on iOS (Objective-C). There is no JavaScript bridge.
5
+ is React-like (function components plus hooks). Python owns the
6
+ component tree, reconciliation, and layout; each commit crosses once
7
+ into a native rendering core (Swift ``PythonNativeKit`` on iOS, the
8
+ Kotlin ``pythonnative`` module on Android) that owns every platform
9
+ view, gesture, animation, and device API. There is no JavaScript.
8
10
 
9
11
  Key building blocks:
10
12
 
@@ -60,7 +62,7 @@ Example:
60
62
  ```
61
63
  """
62
64
 
63
- __version__ = "0.36.0"
65
+ __version__ = "0.37.0"
64
66
 
65
67
  from . import appearance, diagnostics, gestures, images, runtime, sdk
66
68
  from .alerts import Alert
pythonnative/_ios_log.py CHANGED
@@ -14,7 +14,7 @@ straight to fd 2 is a small, reliable fix: fd 2 *is* visible to
14
14
  `simctl` (that is exactly how `NSLog` reaches the terminal), so
15
15
  Python output lands next to the Swift logs with correct ordering.
16
16
 
17
- This module is intentionally self-contained (no rubicon-objc or
17
+ This module is intentionally self-contained (no bridge or
18
18
  platform-specific C bindings required), so it is safe to import
19
19
  early during `pythonnative` package initialization.
20
20
  """
pythonnative/alerts.py CHANGED
@@ -30,77 +30,42 @@ Example:
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
- import asyncio
34
33
  from typing import Any, Dict, List, Optional, Sequence
35
34
 
36
- from .platform import Platform
37
- from .runtime import resolve_future
35
+ from .native_modules.registry import native_module
38
36
 
39
37
  # ======================================================================
40
38
  # Internal dispatch helpers
41
39
  # ======================================================================
42
40
 
43
41
 
44
- def _dispatch_alert(
42
+ async def _present(
45
43
  *,
46
44
  title: str,
47
45
  message: Optional[str],
48
46
  buttons: List[Dict[str, Any]],
49
47
  style: str,
50
- on_result: Any,
51
- ) -> None:
52
- """Route an alert request to the active platform presenter.
48
+ ) -> int:
49
+ """Ask the native ``Alert`` module to present and await the chosen index.
53
50
 
54
51
  ``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.
52
+ "default"|"cancel"|"destructive"}`` dicts. The module resolves with
53
+ the index the user picked, or ``-1`` if the dialog was dismissed
54
+ without a selection. Off device the
55
+ [`DesktopAlert`][pythonnative.native_modules.desktop.DesktopAlert]
56
+ implementation records the call and answers from the queue set by
57
+ [`Alert.set_test_response`][pythonnative.alerts.Alert.set_test_response].
59
58
  """
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)
59
+ try:
60
+ result = await native_module("Alert").call_async(
61
+ "present", title=title, message=message, buttons=buttons, style=style
62
+ )
63
+ except Exception:
64
+ return -1
65
+ try:
66
+ return int(result)
67
+ except (TypeError, ValueError):
68
+ return -1
104
69
 
105
70
 
106
71
  # ======================================================================
@@ -170,13 +135,14 @@ class Alert:
170
135
  [`choose`][pythonnative.alerts.Alert.choose] and ``await``
171
136
  the result.
172
137
  """
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
- )
138
+ try:
139
+ native_module("Alert").call(
140
+ "show", title=title, message=message, buttons=[{"label": button, "style": "default"}], style="alert"
141
+ )
142
+ except Exception:
143
+ from . import diagnostics
144
+
145
+ diagnostics.swallowed("alerts.Alert.show")
180
146
 
181
147
  @staticmethod
182
148
  async def confirm(
@@ -206,13 +172,7 @@ class Alert:
206
172
  await save()
207
173
  ```
208
174
  """
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(
175
+ index = await _present(
216
176
  title=title,
217
177
  message=message,
218
178
  buttons=[
@@ -220,9 +180,8 @@ class Alert:
220
180
  {"label": confirm_label, "style": "default"},
221
181
  ],
222
182
  style="alert",
223
- on_result=_on_result,
224
183
  )
225
- return await future
184
+ return index == 1
226
185
 
227
186
  @staticmethod
228
187
  async def choose(
@@ -265,9 +224,6 @@ class Alert:
265
224
  if not options:
266
225
  raise ValueError("Alert.choose requires at least one option")
267
226
 
268
- loop = asyncio.get_running_loop()
269
- future: asyncio.Future[Optional[str]] = loop.create_future()
270
-
271
227
  destructive = set(destructive_labels)
272
228
  buttons: List[Dict[str, Any]] = [
273
229
  {
@@ -279,20 +235,10 @@ class Alert:
279
235
  if cancel_label is not None:
280
236
  buttons.append({"label": cancel_label, "style": "cancel"})
281
237
 
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
238
+ index = await _present(title=title, message=message, buttons=buttons, style=style)
239
+ if 0 <= index < len(options):
240
+ return options[index]
241
+ return None
296
242
 
297
243
 
298
244
  __all__ = ["Alert"]
@@ -0,0 +1,90 @@
1
+ """Entry point the native app templates run right after Python starts.
2
+
3
+ Both templates execute one line of Python once the interpreter is up:
4
+
5
+ ```python
6
+ import pythonnative.bootstrap; pythonnative.bootstrap.start()
7
+ ```
8
+
9
+ [`start`][pythonnative.bootstrap.start] connects the two halves of the
10
+ bridge (installing the native -> Python callback on iOS), verifies the
11
+ protocol version, routes ``print()`` to the console on iOS, and warms
12
+ the asyncio runtime. From then on the native runtime drives everything
13
+ through ``callback("host", ...)``; see ``docs/concepts/bridge.md``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+ import traceback
21
+ from typing import Any, Dict
22
+
23
+ __all__ = ["start", "status"]
24
+
25
+ _started: Dict[str, Any] = {}
26
+
27
+
28
+ def start(dev: bool = False, strict: bool = False) -> Dict[str, Any]:
29
+ """Connect the bridge and prepare the runtime.
30
+
31
+ Args:
32
+ dev: Enable dev mode (RedBox, validation warnings). Debug
33
+ templates pass ``True``; hot reload turns it on as well.
34
+ strict: Re-raise the failure after recording it. The templates
35
+ pass ``True`` so a broken bridge surfaces as a bootstrap
36
+ error screen with the full traceback.
37
+
38
+ Returns:
39
+ A status dict (``{"protocol": 1, "platform": "ios"}``) that the
40
+ template logs. Unless ``strict`` is set this never raises:
41
+ failures are printed and reported in the dict under
42
+ ``"error"``.
43
+ """
44
+ global _started
45
+ if _started and "error" not in _started:
46
+ return dict(_started)
47
+ status_: Dict[str, Any] = {"platform": None, "protocol": None}
48
+ try:
49
+ from .utils import IS_ANDROID, IS_IOS
50
+
51
+ status_["platform"] = "ios" if IS_IOS else "android" if IS_ANDROID else "off-device"
52
+ if IS_IOS:
53
+ try:
54
+ from . import _ios_log
55
+
56
+ _ios_log.install()
57
+ except Exception:
58
+ pass
59
+ from . import bridge
60
+
61
+ status_["protocol"] = bridge.handshake()
62
+ if dev or os.environ.get("PN_DEV") in ("1", "true"):
63
+ from . import diagnostics
64
+
65
+ diagnostics.set_dev_mode(True)
66
+ # Create the guest loop now so the first pump request has a
67
+ # loop to drive and effects can schedule work during mount.
68
+ from .runtime import get_loop
69
+
70
+ get_loop()
71
+ # Import the view backend eagerly: on a slow device the first
72
+ # commit shouldn't also pay for importing the reconciler.
73
+ from .native_views import get_registry
74
+
75
+ get_registry()
76
+ except Exception as exc:
77
+ status_["error"] = f"{type(exc).__name__}: {exc}"
78
+ print(f"[pn.bootstrap] start failed: {exc!r}", file=sys.stderr)
79
+ traceback.print_exc()
80
+ _started = status_
81
+ if strict:
82
+ raise
83
+ return dict(status_)
84
+ _started = status_
85
+ return dict(status_)
86
+
87
+
88
+ def status() -> Dict[str, Any]:
89
+ """Return the result of the last [`start`][pythonnative.bootstrap.start] (empty before it ran)."""
90
+ return dict(_started)
@@ -0,0 +1,306 @@
1
+ """The native bridge: one channel between Python and Swift / Kotlin.
2
+
3
+ Everything that crosses into native code goes through a
4
+ [`Transport`][pythonnative.bridge.Transport] (``apply`` a transaction,
5
+ ``measure`` a view, run a ``command``, drive an ``animate`` request, or
6
+ ``call`` a native module), and everything native sends back arrives at
7
+ [`native_callback`][pythonnative.bridge.native_callback]. The protocol
8
+ is documented in ``docs/concepts/bridge.md``.
9
+
10
+ Off-device (tests, ``pn preview``) there is no transport; the desktop
11
+ registry renders with Tkinter and native modules fall back to their
12
+ Python implementations. Tests that want to exercise the bridge itself
13
+ install a [`FakeTransport`][pythonnative.bridge.fake.FakeTransport]
14
+ with [`set_transport`][pythonnative.bridge.set_transport].
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import threading
20
+ from collections import deque
21
+ from typing import Any, Callable, Deque, Dict, Optional, Protocol, Tuple
22
+
23
+ from . import codec
24
+
25
+ __all__ = [
26
+ "PROTOCOL_VERSION",
27
+ "Transport",
28
+ "get_transport",
29
+ "has_transport",
30
+ "handshake",
31
+ "native_callback",
32
+ "post_to_main",
33
+ "set_transport",
34
+ ]
35
+
36
+ PROTOCOL_VERSION = 1
37
+ """Bridge protocol version this Python package speaks."""
38
+
39
+
40
+ class Transport(Protocol):
41
+ """The Python -> native half of the bridge."""
42
+
43
+ name: str
44
+
45
+ def protocol_version(self) -> int:
46
+ """Return the protocol version compiled into the native library."""
47
+
48
+ def apply(self, transaction_json: str) -> None:
49
+ """Apply one serialized transaction (a JSON array of ops)."""
50
+
51
+ def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
52
+ """Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
53
+
54
+ def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
55
+ """Run an imperative command on one view; returns its JSON result or ``None``."""
56
+
57
+ def animate(self, tag: int, request_json: str) -> Optional[str]:
58
+ """Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
59
+
60
+ def call(self, module: str, method: str, args_json: str) -> Optional[str]:
61
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
62
+
63
+ def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
64
+ """Install ``callback`` as the native -> Python entry point."""
65
+
66
+
67
+ # ======================================================================
68
+ # Transport selection
69
+ # ======================================================================
70
+
71
+ _transport: Optional[Transport] = None
72
+ _transport_lock = threading.Lock()
73
+ _explicit = False
74
+
75
+
76
+ def _create_platform_transport() -> Optional[Transport]:
77
+ from ..utils import IS_ANDROID, IS_IOS
78
+
79
+ if IS_IOS:
80
+ from .ios import IOSTransport
81
+
82
+ transport: Transport = IOSTransport()
83
+ transport.set_callback(native_callback)
84
+ return transport
85
+ if IS_ANDROID:
86
+ from .android import AndroidTransport
87
+
88
+ return AndroidTransport()
89
+ return None
90
+
91
+
92
+ def get_transport() -> Transport:
93
+ """Return the active transport, creating the platform one on first use.
94
+
95
+ Raises:
96
+ RuntimeError: Off-device, where no native runtime exists.
97
+ """
98
+ global _transport
99
+ if _transport is not None:
100
+ return _transport
101
+ with _transport_lock:
102
+ if _transport is None:
103
+ created = _create_platform_transport()
104
+ if created is None:
105
+ raise RuntimeError(
106
+ "No native bridge is available on this platform (running off-device). "
107
+ "Use `pn preview` for the desktop renderer or install a FakeTransport in tests."
108
+ )
109
+ _transport = created
110
+ return _transport
111
+
112
+
113
+ def has_transport() -> bool:
114
+ """Whether a transport exists or can be created without raising."""
115
+ if _transport is not None:
116
+ return True
117
+ if _explicit:
118
+ return False
119
+ from ..utils import IS_ANDROID, IS_IOS
120
+
121
+ return bool(IS_IOS or IS_ANDROID)
122
+
123
+
124
+ def set_transport(transport: Optional[Transport]) -> None:
125
+ """Install a transport explicitly (tests) or reset with ``None``."""
126
+ global _transport, _explicit
127
+ with _transport_lock:
128
+ _transport = transport
129
+ _explicit = transport is not None
130
+ if transport is not None:
131
+ transport.set_callback(native_callback)
132
+
133
+
134
+ def handshake() -> int:
135
+ """Verify the native library speaks our protocol version.
136
+
137
+ Called by the native templates right after Python starts. Returns
138
+ the negotiated version.
139
+
140
+ Raises:
141
+ RuntimeError: On a version mismatch, with a hint to rebuild.
142
+ """
143
+ version = get_transport().protocol_version()
144
+ if version != PROTOCOL_VERSION:
145
+ raise RuntimeError(
146
+ f"Bridge protocol mismatch: the native runtime speaks v{version} but pythonnative "
147
+ f"expects v{PROTOCOL_VERSION}. Re-run 'pn build' so the staged template matches the "
148
+ "installed pythonnative package."
149
+ )
150
+ return version
151
+
152
+
153
+ # ======================================================================
154
+ # Main-queue posting
155
+ # ======================================================================
156
+ #
157
+ # The asyncio guest loop and ``call_on_main_thread`` need a way to run
158
+ # a callable on the platform main thread's next turn. Native provides
159
+ # it: ``Host.post()`` schedules ``callback("pump")``, which drains this
160
+ # queue. Keeping the queue in Python means one native crossing per
161
+ # batch of callables rather than one per callable.
162
+
163
+ _main_queue: Deque[Callable[[], None]] = deque()
164
+ _main_queue_lock = threading.Lock()
165
+ _pump_requested = False
166
+
167
+
168
+ def post_to_main(fn: Callable[[], None]) -> None:
169
+ """Queue ``fn`` for the next main-thread turn (never runs inline)."""
170
+ global _pump_requested
171
+ with _main_queue_lock:
172
+ _main_queue.append(fn)
173
+ if _pump_requested:
174
+ return
175
+ _pump_requested = True
176
+ try:
177
+ get_transport().call("Host", "post", codec.dumps({"call_id": 0, "args": {}}))
178
+ except Exception as exc:
179
+ with _main_queue_lock:
180
+ _pump_requested = False
181
+ print(f"[pn.bridge] Host.post failed; running {len(_main_queue)} queued callable(s) inline: {exc!r}")
182
+ _drain_main_queue()
183
+
184
+
185
+ def _drain_main_queue() -> None:
186
+ global _pump_requested
187
+ while True:
188
+ with _main_queue_lock:
189
+ if not _main_queue:
190
+ _pump_requested = False
191
+ return
192
+ fn = _main_queue.popleft()
193
+ try:
194
+ fn()
195
+ except Exception as exc:
196
+ print(f"[pn.bridge] main-thread callable raised: {exc!r}")
197
+
198
+
199
+ # ======================================================================
200
+ # native -> Python
201
+ # ======================================================================
202
+
203
+
204
+ def native_callback(kind: str, tag: int, name: str, payload: str) -> Optional[str]:
205
+ """Single entry point for every native -> Python message.
206
+
207
+ Args:
208
+ kind: ``"event"``, ``"module"``, ``"host"``, ``"animation"``,
209
+ or ``"pump"``.
210
+ tag: View tag (events), screen id (host), otherwise ``0``.
211
+ name: Event name, module name, or host event.
212
+ payload: JSON text whose shape depends on ``kind``.
213
+
214
+ Returns:
215
+ A JSON string for request-style messages (a handler's return
216
+ value, ``"true"`` / ``"false"`` for ``back_pressed``), else
217
+ ``None``. Never raises: failures are reported through
218
+ ``diagnostics`` so nothing propagates into UIKit or the
219
+ Android looper.
220
+ """
221
+ try:
222
+ if kind == "event":
223
+ return _on_event(int(tag), name, payload)
224
+ if kind == "module":
225
+ from ..native_modules.registry import dispatch_module_message
226
+
227
+ dispatch_module_message(name, codec.loads(payload) or {})
228
+ return None
229
+ if kind == "host":
230
+ from ..hosts.native import dispatch_host_event
231
+
232
+ return dispatch_host_event(int(tag), name, codec.loads(payload))
233
+ if kind == "animation":
234
+ from ..animated import native_animation_completed
235
+
236
+ data = codec.loads(payload) or {}
237
+ native_animation_completed(int(data.get("id", 0)), bool(data.get("finished", True)))
238
+ return None
239
+ if kind == "pump":
240
+ _drain_main_queue()
241
+ return None
242
+ print(f"[pn.bridge] unknown callback kind {kind!r}")
243
+ except Exception as exc:
244
+ from .. import diagnostics
245
+
246
+ if not diagnostics.report_error(exc, phase=f"bridge {kind}:{name}"):
247
+ import traceback
248
+
249
+ traceback.print_exc()
250
+ return None
251
+
252
+
253
+ def _on_event(tag: int, name: str, payload: str) -> Optional[str]:
254
+ from ..events import get_event_registry
255
+
256
+ args = codec.loads(payload)
257
+ if args is None:
258
+ args = []
259
+ elif not isinstance(args, list):
260
+ args = [args]
261
+ callback = get_event_registry().get(tag, name)
262
+ if callback is None:
263
+ from ..native_views import get_registry
264
+
265
+ backend = get_registry()
266
+ internal = getattr(backend, "handle_internal_event", None)
267
+ if internal is not None:
268
+ result = internal(tag, name, args)
269
+ return None if result is None else codec.dumps(codec.to_jsonable(result))
270
+ return None
271
+ try:
272
+ result = callback(*args)
273
+ except Exception as exc:
274
+ from .. import diagnostics
275
+
276
+ if not diagnostics.report_error(exc, phase=f"event {name!r}"):
277
+ import traceback
278
+
279
+ traceback.print_exc()
280
+ return None
281
+ if result is None:
282
+ return None
283
+ try:
284
+ return codec.dumps(codec.to_jsonable(result))
285
+ except (TypeError, ValueError):
286
+ return None
287
+
288
+
289
+ def _reset_for_tests() -> None:
290
+ """Drop the transport and any queued main-thread work (test isolation)."""
291
+ global _transport, _explicit, _pump_requested
292
+ with _transport_lock:
293
+ _transport = None
294
+ _explicit = False
295
+ with _main_queue_lock:
296
+ _main_queue.clear()
297
+ _pump_requested = False
298
+
299
+
300
+ def transport_state() -> Dict[str, Any]:
301
+ """Diagnostics snapshot (used by ``pn doctor`` and tests)."""
302
+ return {
303
+ "transport": None if _transport is None else _transport.name,
304
+ "protocol_version": PROTOCOL_VERSION,
305
+ "queued_main_callables": len(_main_queue),
306
+ }
@@ -0,0 +1,78 @@
1
+ """Android transport: the ``com.pythonnative.runtime.PNBridge`` class via Chaquopy.
2
+
3
+ ``PNBridge`` is the only Java class Python touches. Its static methods
4
+ mirror the C entry points used on iOS (see ``docs/concepts/bridge.md``).
5
+ The reverse direction (native -> Python) is installed by the template's
6
+ ``MainActivity``, which implements ``PythonHost`` by calling
7
+ [`pythonnative.bridge.native_callback`][pythonnative.bridge.native_callback]
8
+ through Chaquopy; this transport therefore has nothing to register.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Callable, Optional, Tuple
14
+
15
+ __all__ = ["AndroidTransport"]
16
+
17
+ BRIDGE_CLASS = "com.pythonnative.runtime.PNBridge"
18
+
19
+
20
+ class AndroidTransport:
21
+ """Thin wrapper over the static ``PNBridge`` methods."""
22
+
23
+ name = "android"
24
+
25
+ def __init__(self, bridge_class: Any = None) -> None:
26
+ if bridge_class is None:
27
+ from java import jclass
28
+
29
+ try:
30
+ bridge_class = jclass(BRIDGE_CLASS)
31
+ except Exception as exc:
32
+ raise RuntimeError(
33
+ f"{BRIDGE_CLASS} is not on the classpath. Rebuild the app with 'pn run android' or "
34
+ "'pn build android' so the pythonnative Gradle module is included."
35
+ ) from exc
36
+ self._bridge = bridge_class
37
+
38
+ def protocol_version(self) -> int:
39
+ """Return the protocol version compiled into the native library."""
40
+ return int(self._bridge.protocolVersion())
41
+
42
+ def apply(self, transaction_json: str) -> None:
43
+ """Apply one serialized transaction (a JSON array of ops)."""
44
+ self._bridge.apply(transaction_json)
45
+
46
+ def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
47
+ """Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
48
+ packed = self._bridge.measure(int(tag), float(max_width), float(max_height))
49
+ if not packed:
50
+ return (0.0, 0.0)
51
+ text = str(packed)
52
+ w, _, h = text.partition(",")
53
+ try:
54
+ return (float(w), float(h))
55
+ except ValueError:
56
+ return (0.0, 0.0)
57
+
58
+ def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
59
+ """Run an imperative command on one view; returns its JSON result or ``None``."""
60
+ return _opt_str(self._bridge.command(int(tag), name, args_json))
61
+
62
+ def animate(self, tag: int, request_json: str) -> Optional[str]:
63
+ """Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
64
+ return _opt_str(self._bridge.animate(int(tag), request_json))
65
+
66
+ def call(self, module: str, method: str, args_json: str) -> Optional[str]:
67
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
68
+ return _opt_str(self._bridge.call(module, method, args_json))
69
+
70
+ def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
71
+ """No-op: the Android template installs the host through ``PNBridge.setHost``."""
72
+ del callback
73
+
74
+
75
+ def _opt_str(value: Any) -> Optional[str]:
76
+ if value is None:
77
+ return None
78
+ return str(value)