pythonnative 0.36.0__py3-none-any.whl → 0.38.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 (187) hide show
  1. pythonnative/__init__.py +14 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +24 -86
  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/cli/pn.py +96 -10
  11. pythonnative/components/_base.py +31 -0
  12. pythonnative/components/controls.py +19 -16
  13. pythonnative/components/layout.py +70 -11
  14. pythonnative/components/lists.py +6 -6
  15. pythonnative/gestures.py +40 -16
  16. pythonnative/hooks.py +41 -26
  17. pythonnative/hosts/__init__.py +13 -36
  18. pythonnative/hosts/native.py +348 -0
  19. pythonnative/images.py +6 -5
  20. pythonnative/native_modules/__init__.py +49 -29
  21. pythonnative/native_modules/app_state.py +10 -6
  22. pythonnative/native_modules/battery.py +22 -87
  23. pythonnative/native_modules/biometrics.py +15 -130
  24. pythonnative/native_modules/camera.py +24 -296
  25. pythonnative/native_modules/clipboard.py +14 -111
  26. pythonnative/native_modules/desktop.py +322 -0
  27. pythonnative/native_modules/file_system.py +90 -187
  28. pythonnative/native_modules/haptics.py +29 -135
  29. pythonnative/native_modules/linking.py +29 -141
  30. pythonnative/native_modules/location.py +32 -186
  31. pythonnative/native_modules/net_info.py +22 -135
  32. pythonnative/native_modules/notifications.py +37 -265
  33. pythonnative/native_modules/permissions.py +78 -194
  34. pythonnative/native_modules/registry.py +513 -0
  35. pythonnative/native_modules/secure_store.py +23 -164
  36. pythonnative/native_modules/share.py +9 -114
  37. pythonnative/native_views/__init__.py +67 -86
  38. pythonnative/native_views/bridge_backend.py +264 -0
  39. pythonnative/navigation/__init__.py +2 -1
  40. pythonnative/navigation/hooks.py +46 -7
  41. pythonnative/navigation/state.py +30 -9
  42. pythonnative/platform.py +19 -15
  43. pythonnative/platform_metrics.py +3 -3
  44. pythonnative/project/android.py +23 -3
  45. pythonnative/project/builder.py +99 -48
  46. pythonnative/project/config.py +96 -26
  47. pythonnative/project/deps.py +703 -0
  48. pythonnative/project/doctor.py +53 -4
  49. pythonnative/project/plugins.py +366 -0
  50. pythonnative/project/runtime_assets.py +13 -14
  51. pythonnative/py.typed +0 -0
  52. pythonnative/runtime.py +35 -203
  53. pythonnative/sdk/__init__.py +48 -33
  54. pythonnative/sdk/_components.py +57 -76
  55. pythonnative/storage.py +18 -243
  56. pythonnative/style.py +94 -44
  57. pythonnative/templates/android_template/app/build.gradle +11 -11
  58. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  59. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  60. pythonnative/templates/android_template/build.gradle +7 -4
  61. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
  62. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  103. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  105. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  106. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  107. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  108. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  109. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  110. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  111. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  112. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  113. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  114. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  115. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  116. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  117. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  118. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  119. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  120. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  121. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  122. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  123. pythonnative/templates/android_template/settings.gradle +1 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  157. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  158. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  159. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  160. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  161. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  162. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  163. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  164. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  165. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  166. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  167. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  168. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  169. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  170. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +41 -48
  171. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  172. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  173. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +82 -10
  174. pythonnative/utils.py +26 -109
  175. pythonnative/virtual_rows.py +13 -23
  176. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +10 -12
  177. pythonnative-0.38.0.dist-info/RECORD +264 -0
  178. pythonnative/hosts/android.py +0 -269
  179. pythonnative/hosts/ios.py +0 -404
  180. pythonnative/native_views/android.py +0 -3702
  181. pythonnative/native_views/ios.py +0 -5253
  182. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  183. pythonnative-0.36.0.dist-info/RECORD +0 -155
  184. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/WHEEL +0 -0
  185. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
  186. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
  187. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -0,0 +1,164 @@
1
+ """JSON encoding for the native bridge.
2
+
3
+ The bridge speaks JSON in both directions (see ``docs/concepts/bridge.md``).
4
+ Prop values coming out of the reconciler are almost, but not quite,
5
+ JSON: they may contain ``frozenset`` event-name sets, tuples, floats
6
+ that are infinite, and Python callables that native must never see.
7
+ [`to_jsonable`][pythonnative.bridge.codec.to_jsonable] normalizes a
8
+ value into something ``json.dumps`` accepts and reports the keys it had
9
+ to drop so the backend can keep them Python-side.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import math
16
+ from typing import Any, Dict, List, Sequence, Tuple, Union
17
+
18
+ from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, SetFrameOp, UpdateOp
19
+
20
+ __all__ = [
21
+ "INF",
22
+ "NEG_INF",
23
+ "dumps",
24
+ "encode_transaction",
25
+ "loads",
26
+ "split_props",
27
+ "to_jsonable",
28
+ ]
29
+
30
+ INF = "inf"
31
+ """Wire spelling of ``math.inf`` (JSON has no infinity literal)."""
32
+
33
+ NEG_INF = "-inf"
34
+ """Wire spelling of ``-math.inf``."""
35
+
36
+ _SCALARS = (str, int, bool, type(None))
37
+
38
+
39
+ def to_jsonable(value: Any) -> Any:
40
+ """Return a JSON-serializable copy of ``value``.
41
+
42
+ - ``set`` / ``frozenset`` / ``tuple`` become lists (sets are sorted
43
+ when their members are strings so the output is deterministic).
44
+ - Infinite floats become the strings ``"inf"`` / ``"-inf"``; NaN
45
+ becomes ``None`` (native clamps missing geometry to zero).
46
+ - Dataclass-like objects with ``to_json()`` are converted through
47
+ it.
48
+
49
+ Raises:
50
+ TypeError: When ``value`` (or something nested in it) has no
51
+ JSON representation; callers decide whether to drop it.
52
+ """
53
+ if isinstance(value, _SCALARS):
54
+ return value
55
+ if isinstance(value, float):
56
+ if math.isfinite(value):
57
+ return value
58
+ if math.isnan(value):
59
+ return None
60
+ return INF if value > 0 else NEG_INF
61
+ if isinstance(value, dict):
62
+ return {str(k): to_jsonable(v) for k, v in value.items()}
63
+ if isinstance(value, (list, tuple)):
64
+ return [to_jsonable(v) for v in value]
65
+ if isinstance(value, (set, frozenset)):
66
+ items = list(value)
67
+ try:
68
+ items.sort()
69
+ except TypeError:
70
+ pass
71
+ return [to_jsonable(v) for v in items]
72
+ to_json = getattr(value, "to_json", None)
73
+ if callable(to_json):
74
+ return to_jsonable(to_json())
75
+ raise TypeError(f"value of type {type(value).__name__} is not bridge-serializable")
76
+
77
+
78
+ def split_props(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
79
+ """Split ``props`` into ``(wire_props, python_props)``.
80
+
81
+ ``wire_props`` is JSON-ready; ``python_props`` holds every prop
82
+ that can't cross the bridge (callables such as ``render_row``,
83
+ arbitrary objects). The backend keeps the latter in a per-tag
84
+ sidecar so native-backed handlers that need them (virtualized
85
+ list rows) can still reach them.
86
+ """
87
+ wire: Dict[str, Any] = {}
88
+ python: Dict[str, Any] = {}
89
+ for key, value in props.items():
90
+ if callable(value) and not isinstance(value, type):
91
+ python[key] = value
92
+ continue
93
+ try:
94
+ wire[key] = to_jsonable(value)
95
+ except TypeError:
96
+ python[key] = value
97
+ return wire, python
98
+
99
+
100
+ def encode_transaction(
101
+ ops: Sequence[Mutation], prop_filter: Any = None
102
+ ) -> Tuple[str, List[Tuple[int, Dict[str, Any]]]]:
103
+ """Encode a mutation batch as the bridge's transaction JSON.
104
+
105
+ Args:
106
+ ops: Ordered mutations from the reconciler.
107
+ prop_filter: Unused hook kept for symmetry with the desktop
108
+ registry; reserved for per-type prop rewriting.
109
+
110
+ Returns:
111
+ ``(json_text, python_props)`` where ``python_props`` lists
112
+ ``(tag, props)`` pairs for values that stayed Python-side (for
113
+ ``c`` and ``u`` ops). An update that *removes* a Python-side
114
+ prop reports it as ``None`` so the sidecar can drop it.
115
+ """
116
+ del prop_filter
117
+ encoded: List[Any] = []
118
+ sidecar: List[Tuple[int, Dict[str, Any]]] = []
119
+ for op in ops:
120
+ if isinstance(op, CreateOp):
121
+ wire, python = split_props(op.props)
122
+ encoded.append(["c", op.tag, op.type_name, wire])
123
+ if python:
124
+ sidecar.append((op.tag, python))
125
+ elif isinstance(op, UpdateOp):
126
+ wire, python = split_props(op.changed_props)
127
+ encoded.append(["u", op.tag, wire])
128
+ if python:
129
+ sidecar.append((op.tag, python))
130
+ elif isinstance(op, InsertOp):
131
+ encoded.append(["i", op.parent_tag, op.child_tag, op.index])
132
+ elif isinstance(op, DestroyOp):
133
+ encoded.append(["d", op.tag])
134
+ elif isinstance(op, SetFrameOp):
135
+ encoded.append(["f", op.tag, _num(op.x), _num(op.y), _num(op.width), _num(op.height)])
136
+ else: # pragma: no cover - the reconciler only emits the five op kinds
137
+ raise TypeError(f"Unknown mutation op: {op!r}")
138
+ return dumps(encoded), sidecar
139
+
140
+
141
+ def _num(value: float) -> float:
142
+ """Clamp a frame coordinate to a finite float (native treats NaN/inf as 0)."""
143
+ try:
144
+ f = float(value)
145
+ except (TypeError, ValueError):
146
+ return 0.0
147
+ return f if math.isfinite(f) else 0.0
148
+
149
+
150
+ def dumps(value: Any) -> str:
151
+ """Compact ``json.dumps`` used for every bridge payload."""
152
+ return json.dumps(value, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
153
+
154
+
155
+ def loads(text: Union[str, bytes, bytearray, None]) -> Any:
156
+ """Parse a bridge payload (``str`` or UTF-8 bytes); empty / ``None`` input yields ``None``."""
157
+ if text is None:
158
+ return None
159
+ if isinstance(text, (bytes, bytearray)):
160
+ text = text.decode("utf-8")
161
+ text = text.strip()
162
+ if not text:
163
+ return None
164
+ return json.loads(text)
@@ -0,0 +1,236 @@
1
+ """An in-process stand-in for the native side of the bridge.
2
+
3
+ [`FakeTransport`][pythonnative.bridge.fake.FakeTransport] decodes the
4
+ same JSON the Swift and Kotlin runtimes receive and keeps a tiny view
5
+ tree so tests can assert on what *would* have reached native: created
6
+ types and props, insert order, frames, commands, animation requests,
7
+ and module calls. It also lets tests play the native side, firing
8
+ events and module results back through
9
+ [`native_callback`][pythonnative.bridge.native_callback].
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import Any, Callable, Dict, List, Optional, Tuple
16
+
17
+ from . import codec
18
+
19
+ __all__ = ["FakeNativeView", "FakeTransport"]
20
+
21
+
22
+ class FakeNativeView:
23
+ """One decoded native view: type, merged props, children, and frame."""
24
+
25
+ def __init__(self, tag: int, type_name: str, props: Dict[str, Any]) -> None:
26
+ self.tag = tag
27
+ self.type_name = type_name
28
+ self.props: Dict[str, Any] = dict(props)
29
+ self.children: List["FakeNativeView"] = []
30
+ self.parent: Optional["FakeNativeView"] = None
31
+ self.frame: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0)
32
+
33
+ def __repr__(self) -> str:
34
+ return f"<native {self.type_name} tag={self.tag}>"
35
+
36
+
37
+ ModuleHandler = Callable[[str, Dict[str, Any]], Any]
38
+ """``handler(method, args) -> value`` for a fake native module."""
39
+
40
+ DEFAULT_MEASURE: Dict[str, Tuple[float, float]] = {
41
+ "Text": (60.0, 16.0),
42
+ "Button": (80.0, 32.0),
43
+ "Image": (40.0, 40.0),
44
+ "TextInput": (120.0, 32.0),
45
+ "TabBar": (320.0, 49.0),
46
+ }
47
+
48
+
49
+ class FakeTransport:
50
+ """Decode bridge traffic into inspectable Python structures.
51
+
52
+ Attributes:
53
+ views: Live views by tag.
54
+ transactions: Every decoded transaction (list of op lists).
55
+ commands: ``(tag, name, args)`` tuples in call order.
56
+ animations: ``(tag, request)`` tuples in call order.
57
+ calls: ``(module, method, args)`` tuples in call order.
58
+ """
59
+
60
+ name = "fake"
61
+
62
+ def __init__(self, *, version: int = 1, measure: Optional[Dict[str, Tuple[float, float]]] = None) -> None:
63
+ self._version = version
64
+ self.measure_table = dict(DEFAULT_MEASURE if measure is None else measure)
65
+ self.views: Dict[int, FakeNativeView] = {}
66
+ self.transactions: List[List[Any]] = []
67
+ self.commands: List[Tuple[int, str, Dict[str, Any]]] = []
68
+ self.animations: List[Tuple[int, Dict[str, Any]]] = []
69
+ self.calls: List[Tuple[str, str, Dict[str, Any]]] = []
70
+ self.command_results: Dict[str, Any] = {}
71
+ self.animate_results: Dict[str, Any] = {"start": {"ok": True}}
72
+ self.module_handlers: Dict[str, ModuleHandler] = {}
73
+ self.pending_calls: List[Tuple[str, int, str, Dict[str, Any]]] = []
74
+ self.callback: Optional[Callable[[str, int, str, str], Optional[str]]] = None
75
+ self.posted = 0
76
+
77
+ # -- Transport protocol ----------------------------------------------
78
+
79
+ def protocol_version(self) -> int:
80
+ """Return the protocol version compiled into the native library."""
81
+ return self._version
82
+
83
+ def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
84
+ """Install ``callback`` as the native -> Python entry point."""
85
+ self.callback = callback
86
+
87
+ def apply(self, transaction_json: str) -> None:
88
+ """Apply one serialized transaction (a JSON array of ops)."""
89
+ ops = json.loads(transaction_json)
90
+ self.transactions.append(ops)
91
+ for op in ops:
92
+ self._apply_one(op)
93
+
94
+ def _apply_one(self, op: List[Any]) -> None:
95
+ code = op[0]
96
+ if code == "c":
97
+ _, tag, type_name, props = op
98
+ if tag in self.views:
99
+ raise AssertionError(f"create: tag {tag} already exists")
100
+ self.views[tag] = FakeNativeView(tag, type_name, props)
101
+ elif code == "u":
102
+ _, tag, changed = op
103
+ view = self._require(tag, "update")
104
+ for key, value in changed.items():
105
+ if value is None:
106
+ view.props.pop(key, None)
107
+ else:
108
+ view.props[key] = value
109
+ elif code == "i":
110
+ _, parent_tag, child_tag, index = op
111
+ parent = self._require(parent_tag, "insert")
112
+ child = self._require(child_tag, "insert")
113
+ if child.parent is not None:
114
+ child.parent.children.remove(child)
115
+ index = max(0, min(int(index), len(parent.children)))
116
+ parent.children.insert(index, child)
117
+ child.parent = parent
118
+ elif code == "d":
119
+ _, tag = op
120
+ view = self.views.pop(tag, None)
121
+ if view is None:
122
+ raise AssertionError(f"destroy: unknown tag {tag}")
123
+ if view.parent is not None:
124
+ view.parent.children.remove(view)
125
+ view.parent = None
126
+ elif code == "f":
127
+ _, tag, x, y, w, h = op
128
+ self._require(tag, "set_frame").frame = (float(x), float(y), float(w), float(h))
129
+ else:
130
+ raise AssertionError(f"unknown opcode {code!r}")
131
+
132
+ def _require(self, tag: int, what: str) -> FakeNativeView:
133
+ view = self.views.get(tag)
134
+ if view is None:
135
+ raise AssertionError(f"{what}: unknown tag {tag}")
136
+ return view
137
+
138
+ def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
139
+ """Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
140
+ view = self.views.get(tag)
141
+ if view is None:
142
+ return (0.0, 0.0)
143
+ return self.measure_table.get(view.type_name, (0.0, 0.0))
144
+
145
+ def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
146
+ """Run an imperative command on one view; returns its JSON result or ``None``."""
147
+ args = codec.loads(args_json) or {}
148
+ self.commands.append((tag, name, args))
149
+ result = self.command_results.get(name)
150
+ return None if result is None else codec.dumps(result)
151
+
152
+ def animate(self, tag: int, request_json: str) -> Optional[str]:
153
+ """Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
154
+ request = codec.loads(request_json) or {}
155
+ self.animations.append((tag, request))
156
+ result = self.animate_results.get(str(request.get("op")))
157
+ return None if result is None else codec.dumps(result)
158
+
159
+ def call(self, module: str, method: str, args_json: str) -> Optional[str]:
160
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
161
+ envelope = codec.loads(args_json) or {}
162
+ args = envelope.get("args") or {}
163
+ call_id = int(envelope.get("call_id", 0) or 0)
164
+ self.calls.append((module, method, args))
165
+ if module == "Host" and method == "post":
166
+ self.posted += 1
167
+ return codec.dumps({"ok": True, "value": None})
168
+ handler = self.module_handlers.get(module)
169
+ if handler is None:
170
+ return codec.dumps({"ok": True, "value": None})
171
+ try:
172
+ value = handler(method, args)
173
+ except Exception as exc:
174
+ return codec.dumps({"ok": False, "error": str(exc)})
175
+ if value is _PENDING:
176
+ self.pending_calls.append((module, call_id, method, args))
177
+ return codec.dumps({"pending": True})
178
+ return codec.dumps({"ok": True, "value": codec.to_jsonable(value)})
179
+
180
+ # -- Playing the native side ------------------------------------------
181
+
182
+ def fire(self, tag: int, name: str, *args: Any) -> Any:
183
+ """Emit a view event as native would; returns the decoded handler result."""
184
+ return self._callback("event", tag, name, codec.dumps([codec.to_jsonable(a) for a in args]))
185
+
186
+ def emit_module_event(self, module: str, event: str, payload: Any = None) -> None:
187
+ """Push an unsolicited module event (``AppState`` ``change``, ...) into Python."""
188
+ self._callback("module", 0, module, codec.dumps({"event": event, "payload": codec.to_jsonable(payload)}))
189
+
190
+ def resolve_pending(self, call_id: int, value: Any = None, *, error: Optional[str] = None) -> None:
191
+ """Settle a call that returned ``pending``."""
192
+ module = next((m for m, cid, _, _ in self.pending_calls if cid == call_id), None)
193
+ if module is None:
194
+ raise AssertionError(f"no pending call with id {call_id}")
195
+ self.pending_calls = [p for p in self.pending_calls if p[1] != call_id]
196
+ body: Dict[str, Any] = {"call_id": call_id, "ok": error is None}
197
+ if error is None:
198
+ body["value"] = codec.to_jsonable(value)
199
+ else:
200
+ body["error"] = error
201
+ self._callback("module", 0, module, codec.dumps(body))
202
+
203
+ def host_event(self, screen: int, event: str, payload: Any = None) -> Any:
204
+ """Deliver a screen lifecycle event as the native host would; returns the decoded result."""
205
+ return self._callback("host", screen, event, codec.dumps(codec.to_jsonable(payload)))
206
+
207
+ def pump(self) -> None:
208
+ """Deliver the ``pump`` callback (what ``Host.post`` would trigger)."""
209
+ self._callback("pump", 0, "", "")
210
+
211
+ def complete_animation(self, anim_id: int, finished: bool = True) -> None:
212
+ """Report a native animation as finished (or interrupted) to Python."""
213
+ self._callback("animation", 0, "", codec.dumps({"id": anim_id, "finished": finished}))
214
+
215
+ def _callback(self, kind: str, tag: int, name: str, payload: str) -> Any:
216
+ if self.callback is None:
217
+ raise AssertionError("FakeTransport has no callback; install it with pythonnative.bridge.set_transport")
218
+ return codec.loads(self.callback(kind, tag, name, payload))
219
+
220
+ # -- Introspection -----------------------------------------------------
221
+
222
+ def find(self, type_name: str) -> List[FakeNativeView]:
223
+ """Every live view whose type is ``type_name``, in creation order."""
224
+ return [v for v in self.views.values() if v.type_name == type_name]
225
+
226
+ def roots(self) -> List[FakeNativeView]:
227
+ """Views with no parent (the attached screen roots and detached subtrees)."""
228
+ return [v for v in self.views.values() if v.parent is None]
229
+
230
+ @staticmethod
231
+ def pending() -> object:
232
+ """Sentinel a module handler returns to answer ``pending``."""
233
+ return _PENDING
234
+
235
+
236
+ _PENDING = object()
@@ -0,0 +1,138 @@
1
+ """iOS transport: C-ABI calls into ``PythonNativeKit`` through ``ctypes``.
2
+
3
+ The Swift package exports ``@_cdecl`` symbols (see
4
+ ``docs/concepts/bridge.md``). They're resolved from the running process
5
+ with ``ctypes.PyDLL(None)``: ``PyDLL`` keeps the GIL held across the
6
+ call, so the main thread is never parked while native applies a
7
+ transaction, and the Python callback registered with
8
+ ``pn_bridge_set_callback`` can re-enter Python safely.
9
+
10
+ Strings returned by native are ``strdup``'d; this module copies them
11
+ and hands the pointer back to ``pn_bridge_free``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import ctypes
17
+ from typing import Any, Callable, Optional, Tuple
18
+
19
+ __all__ = ["IOSTransport"]
20
+
21
+ # const char *(*)(const char *kind, int64_t tag, const char *name, const char *payload_json)
22
+ _CALLBACK_TYPE = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int64, ctypes.c_char_p, ctypes.c_char_p)
23
+
24
+
25
+ class IOSTransport:
26
+ """Bind the ``pn_bridge_*`` symbols and expose them as Python methods."""
27
+
28
+ name = "ios"
29
+
30
+ def __init__(self, lib: Any = None) -> None:
31
+ self._lib = lib if lib is not None else ctypes.PyDLL(None)
32
+ self._apply = self._sym("pn_bridge_apply", None, [ctypes.c_char_p])
33
+ self._measure = self._sym(
34
+ "pn_bridge_measure",
35
+ None,
36
+ [
37
+ ctypes.c_int64,
38
+ ctypes.c_double,
39
+ ctypes.c_double,
40
+ ctypes.POINTER(ctypes.c_double),
41
+ ctypes.POINTER(ctypes.c_double),
42
+ ],
43
+ )
44
+ self._command = self._sym(
45
+ "pn_bridge_command", ctypes.c_void_p, [ctypes.c_int64, ctypes.c_char_p, ctypes.c_char_p]
46
+ )
47
+ self._animate = self._sym("pn_bridge_animate", ctypes.c_void_p, [ctypes.c_int64, ctypes.c_char_p])
48
+ self._call = self._sym("pn_bridge_call", ctypes.c_void_p, [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p])
49
+ self._free = self._sym("pn_bridge_free", None, [ctypes.c_void_p])
50
+ self._set_callback = self._sym("pn_bridge_set_callback", None, [_CALLBACK_TYPE])
51
+ self._version = self._sym("pn_bridge_protocol_version", ctypes.c_int, [])
52
+ # Strong references so the C trampoline and its last return
53
+ # buffer outlive the calls native makes into them.
54
+ self._callback_c: Any = None
55
+ self._last_result: Any = None
56
+ self._out_w = ctypes.c_double(0.0)
57
+ self._out_h = ctypes.c_double(0.0)
58
+
59
+ def _sym(self, name: str, restype: Any, argtypes: Any) -> Any:
60
+ try:
61
+ fn = getattr(self._lib, name)
62
+ except AttributeError as exc:
63
+ raise RuntimeError(
64
+ f"PythonNativeKit symbol {name!r} is missing from the app binary. Rebuild the app with "
65
+ "'pn run ios' or 'pn build ios' so the Swift package is linked."
66
+ ) from exc
67
+ fn.restype = restype
68
+ fn.argtypes = argtypes
69
+ return fn
70
+
71
+ # -- protocol -------------------------------------------------------
72
+
73
+ def protocol_version(self) -> int:
74
+ """Return the protocol version compiled into the native library."""
75
+ return int(self._version())
76
+
77
+ def apply(self, transaction_json: str) -> None:
78
+ """Apply one serialized transaction (a JSON array of ops)."""
79
+ self._apply(transaction_json.encode("utf-8"))
80
+
81
+ def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
82
+ """Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
83
+ self._measure(
84
+ int(tag), float(max_width), float(max_height), ctypes.byref(self._out_w), ctypes.byref(self._out_h)
85
+ )
86
+ return (float(self._out_w.value), float(self._out_h.value))
87
+
88
+ def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
89
+ """Run an imperative command on one view; returns its JSON result or ``None``."""
90
+ return self._take(self._command(int(tag), name.encode("utf-8"), args_json.encode("utf-8")))
91
+
92
+ def animate(self, tag: int, request_json: str) -> Optional[str]:
93
+ """Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
94
+ return self._take(self._animate(int(tag), request_json.encode("utf-8")))
95
+
96
+ def call(self, module: str, method: str, args_json: str) -> Optional[str]:
97
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
98
+ return self._take(self._call(module.encode("utf-8"), method.encode("utf-8"), args_json.encode("utf-8")))
99
+
100
+ def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
101
+ """Install ``callback`` as the native -> Python entry point."""
102
+
103
+ def trampoline(kind: bytes, tag: int, name: bytes, payload: bytes) -> Optional[int]:
104
+ try:
105
+ result = callback(
106
+ _decode(kind),
107
+ int(tag),
108
+ _decode(name),
109
+ _decode(payload),
110
+ )
111
+ except Exception as exc: # pragma: no cover - last-resort guard
112
+ print(f"[pn.bridge] callback raised: {exc!r}")
113
+ return None
114
+ if result is None:
115
+ return None
116
+ buf = ctypes.create_string_buffer(result.encode("utf-8"))
117
+ self._last_result = buf
118
+ return ctypes.addressof(buf)
119
+
120
+ self._callback_c = _CALLBACK_TYPE(trampoline)
121
+ self._set_callback(self._callback_c)
122
+
123
+ # -- helpers --------------------------------------------------------
124
+
125
+ def _take(self, ptr: Optional[int]) -> Optional[str]:
126
+ """Copy a native ``char*`` result and free it."""
127
+ if not ptr:
128
+ return None
129
+ try:
130
+ return ctypes.string_at(ptr).decode("utf-8")
131
+ finally:
132
+ self._free(ptr)
133
+
134
+
135
+ def _decode(value: Optional[bytes]) -> str:
136
+ if not value:
137
+ return ""
138
+ return value.decode("utf-8", "replace")