pythonnative 0.35.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 (176) 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/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,264 @@
1
+ """The on-device view backend: every mutation crosses the bridge as one transaction.
2
+
3
+ [`BridgeBackend`][pythonnative.native_views.bridge_backend.BridgeBackend]
4
+ implements the same protocol the reconciler expects from
5
+ [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
6
+ (``apply_mutations``, ``resolve_view``, ``measure_intrinsic``,
7
+ ``command``, and the animation hooks) but owns no native objects. It
8
+ serializes each commit with
9
+ [`encode_transaction`][pythonnative.bridge.codec.encode_transaction]
10
+ and hands the JSON to the platform transport; Swift and Kotlin
11
+ component managers do the rest.
12
+
13
+ Python keeps two small pieces of per-tag state:
14
+
15
+ - A **prop sidecar** for values that can't cross the bridge (the
16
+ ``render_row`` callable of a ``VirtualList``).
17
+ - **Row pools** for virtualized lists, so the synchronous
18
+ ``on_bind_row`` protocol can mount a row subtree and answer with its
19
+ root tag.
20
+
21
+ Views are addressed by tag everywhere. ``resolve_view`` returns a
22
+ [`NativeViewRef`][pythonnative.native_views.bridge_backend.NativeViewRef],
23
+ the opaque handle refs receive.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import math
29
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
30
+
31
+ from ..bridge import codec, get_transport
32
+ from ..mutations import CreateOp, DestroyOp, Mutation, UpdateOp
33
+
34
+ __all__ = ["BridgeBackend", "NativeViewRef"]
35
+
36
+ _MEASURE_UNBOUNDED = 1e6
37
+
38
+
39
+ class NativeViewRef:
40
+ """Opaque handle to a native view living on the other side of the bridge.
41
+
42
+ Attributes:
43
+ tag: The reconciler-assigned tag; pass it to
44
+ ``Reconciler.dispatch_command`` or ``get_registry().command``.
45
+ type_name: The element type (``"Text"``, ``"ScrollView"``, ...).
46
+ """
47
+
48
+ __slots__ = ("tag", "type_name")
49
+
50
+ def __init__(self, tag: int, type_name: str) -> None:
51
+ self.tag = tag
52
+ self.type_name = type_name
53
+
54
+ def __repr__(self) -> str:
55
+ return f"<NativeViewRef {self.type_name} tag={self.tag}>"
56
+
57
+ def __int__(self) -> int:
58
+ return self.tag
59
+
60
+
61
+ class BridgeBackend:
62
+ """Registry protocol implementation that forwards to the native runtime."""
63
+
64
+ def __init__(self, transport: Any = None) -> None:
65
+ self._transport = transport
66
+ self._types: Dict[int, str] = {}
67
+ self._refs: Dict[int, NativeViewRef] = {}
68
+ self._python_props: Dict[int, Dict[str, Any]] = {}
69
+ self._row_pools: Dict[int, Any] = {}
70
+ self._handlers: Dict[str, Any] = {}
71
+
72
+ @property
73
+ def transport(self) -> Any:
74
+ """The transport in use (resolved lazily on first access)."""
75
+ if self._transport is None:
76
+ self._transport = get_transport()
77
+ return self._transport
78
+
79
+ # ------------------------------------------------------------------
80
+ # Registration (kept for protocol parity with NativeViewRegistry)
81
+ # ------------------------------------------------------------------
82
+
83
+ def register(self, type_name: str, handler: Any) -> None:
84
+ """Record a Python handler for diagnostics only.
85
+
86
+ On device, rendering is native; a Python ``ViewHandler`` can't
87
+ create platform views. The registration is kept so
88
+ ``handler_for`` can answer introspection questions and so the
89
+ SDK's install step doesn't fail, but it is never invoked.
90
+ """
91
+ self._handlers[type_name] = handler
92
+
93
+ def handler_for(self, type_name: str) -> Any:
94
+ """Return the diagnostic Python handler registered for ``type_name``."""
95
+ return self._handlers.get(type_name)
96
+
97
+ # ------------------------------------------------------------------
98
+ # Tag table
99
+ # ------------------------------------------------------------------
100
+
101
+ def resolve_view(self, tag: int) -> Optional[NativeViewRef]:
102
+ """Return the handle for ``tag`` (``None`` if the view is gone)."""
103
+ return self._refs.get(tag)
104
+
105
+ def type_of(self, tag: int) -> Optional[str]:
106
+ """Return the element type registered for ``tag``."""
107
+ return self._types.get(tag)
108
+
109
+ def live_view_count(self) -> int:
110
+ """Number of views currently alive on the native side."""
111
+ return len(self._types)
112
+
113
+ def python_props(self, tag: int) -> Dict[str, Any]:
114
+ """Props held Python-side for ``tag`` (callables never sent to native)."""
115
+ return self._python_props.get(tag, {})
116
+
117
+ # ------------------------------------------------------------------
118
+ # Commit channel
119
+ # ------------------------------------------------------------------
120
+
121
+ def apply_mutations(self, ops: Sequence[Mutation]) -> None:
122
+ """Serialize ``ops`` and apply them natively in one crossing."""
123
+ if not ops:
124
+ return
125
+ # Bookkeeping first so a native failure never leaves Python
126
+ # believing a destroyed tag is still live.
127
+ destroyed: List[int] = []
128
+ for op in ops:
129
+ if isinstance(op, CreateOp):
130
+ self._types[op.tag] = op.type_name
131
+ self._refs[op.tag] = NativeViewRef(op.tag, op.type_name)
132
+ elif isinstance(op, DestroyOp):
133
+ destroyed.append(op.tag)
134
+ payload, sidecar = codec.encode_transaction(ops)
135
+ for tag, props in sidecar:
136
+ self._python_props.setdefault(tag, {}).update(props)
137
+ for op in ops:
138
+ if isinstance(op, UpdateOp):
139
+ # A prop that changed from a callable to None arrives
140
+ # as None in the wire dict; drop the stale sidecar copy.
141
+ bucket = self._python_props.get(op.tag)
142
+ if bucket:
143
+ for key, value in op.changed_props.items():
144
+ if value is None:
145
+ bucket.pop(key, None)
146
+ if not bucket:
147
+ self._python_props.pop(op.tag, None)
148
+ self.transport.apply(payload)
149
+ for tag in destroyed:
150
+ self._forget(tag)
151
+
152
+ def _forget(self, tag: int) -> None:
153
+ self._types.pop(tag, None)
154
+ self._refs.pop(tag, None)
155
+ self._python_props.pop(tag, None)
156
+ pool = self._row_pools.pop(tag, None)
157
+ if pool is not None:
158
+ pool.release_all()
159
+
160
+ # ------------------------------------------------------------------
161
+ # Imperative escape hatches
162
+ # ------------------------------------------------------------------
163
+
164
+ def measure_intrinsic(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
165
+ """Ask native for the natural size of ``tag`` under the constraints."""
166
+ if tag not in self._types:
167
+ return (0.0, 0.0)
168
+ w, h = self.transport.measure(tag, _bound(max_width), _bound(max_height))
169
+ return (max(0.0, float(w)), max(0.0, float(h)))
170
+
171
+ def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
172
+ """Run an imperative command on one view; returns its JSON result or ``None``."""
173
+ if tag not in self._types:
174
+ return None
175
+ result = self.transport.command(tag, name, codec.dumps(codec.to_jsonable(args or {})))
176
+ return codec.loads(result)
177
+
178
+ def set_animated_property(self, tag: int, prop_name: str, value: Any) -> None:
179
+ """Write one animated property value without animating (a Python-driven frame)."""
180
+ if tag not in self._types:
181
+ return
182
+ self.transport.animate(tag, codec.dumps({"op": "set", "prop": prop_name, "value": codec.to_jsonable(value)}))
183
+
184
+ def start_animation(self, tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool:
185
+ """Start a native-driven animation; returns whether native accepted it."""
186
+ if tag not in self._types:
187
+ return False
188
+ request = {"op": "start", "id": int(anim_id), "prop": prop_name, "spec": codec.to_jsonable(spec)}
189
+ result = codec.loads(self.transport.animate(tag, codec.dumps(request)))
190
+ return bool(isinstance(result, dict) and result.get("ok"))
191
+
192
+ def cancel_animation(self, tag: int, anim_id: int) -> Any:
193
+ """Cancel a native animation and return its presentation value, if known."""
194
+ if tag not in self._types:
195
+ return None
196
+ result = codec.loads(self.transport.animate(tag, codec.dumps({"op": "cancel", "id": int(anim_id)})))
197
+ if isinstance(result, dict):
198
+ return result.get("value")
199
+ return None
200
+
201
+ # ------------------------------------------------------------------
202
+ # Internal (native-originated) events
203
+ # ------------------------------------------------------------------
204
+
205
+ def handle_internal_event(self, tag: int, name: str, args: List[Any]) -> Any:
206
+ """Serve request-style events that no user callback owns.
207
+
208
+ Currently the virtualized-list row protocol:
209
+
210
+ - ``on_bind_row`` ``[{"container", "index", "width", "height"}]``
211
+ mounts or rebinds the row subtree and returns ``{"root": tag}``.
212
+ - ``on_unbind_row`` ``[{"container"}]`` releases it.
213
+ """
214
+ if name == "on_bind_row":
215
+ return self._bind_row(tag, args[0] if args else {})
216
+ if name == "on_unbind_row":
217
+ info = args[0] if args else {}
218
+ pool = self._row_pools.get(tag)
219
+ if pool is not None and isinstance(info, dict):
220
+ pool.release(int(info.get("container", 0)))
221
+ return None
222
+ return None
223
+
224
+ def _bind_row(self, tag: int, info: Any) -> Dict[str, Any]:
225
+ if not isinstance(info, dict):
226
+ return {"root": None}
227
+ render_row = self._python_props.get(tag, {}).get("render_row")
228
+ if not callable(render_row):
229
+ return {"root": None}
230
+ from ..virtual_rows import RowHostPool
231
+
232
+ pool = self._row_pools.get(tag)
233
+ if pool is None:
234
+ pool = RowHostPool()
235
+ self._row_pools[tag] = pool
236
+ index = int(info.get("index", 0))
237
+ width = float(info.get("width", 0.0) or 0.0)
238
+ height = float(info.get("height", 0.0) or 0.0)
239
+ root = pool.bind(int(info.get("container", 0)), lambda: render_row(index), width, height)
240
+ root_tag = getattr(root, "tag", None)
241
+ return {"root": int(root_tag) if root_tag is not None else None}
242
+
243
+ # ------------------------------------------------------------------
244
+ # Test helpers
245
+ # ------------------------------------------------------------------
246
+
247
+ def _reset_for_tests(self) -> None:
248
+ for pool in list(self._row_pools.values()):
249
+ pool.release_all()
250
+ self._row_pools.clear()
251
+ self._types.clear()
252
+ self._refs.clear()
253
+ self._python_props.clear()
254
+
255
+
256
+ def _bound(value: float) -> float:
257
+ """Clamp ``math.inf`` to the wire's "unconstrained" sentinel."""
258
+ try:
259
+ f = float(value)
260
+ except (TypeError, ValueError):
261
+ return _MEASURE_UNBOUNDED
262
+ if not math.isfinite(f) or f > _MEASURE_UNBOUNDED:
263
+ return _MEASURE_UNBOUNDED
264
+ return max(0.0, f)
pythonnative/platform.py CHANGED
@@ -43,33 +43,37 @@ def _detect_os() -> str:
43
43
  def _detect_version() -> str:
44
44
  """Return a human-readable platform version string.
45
45
 
46
- On iOS, reads the Simulator/device's reported OS version. On
47
- Android, reads ``Build.VERSION.RELEASE``. Off-device (the test
46
+ On device this asks the native ``Device`` module (``UIDevice
47
+ .systemVersion`` / ``Build.VERSION.RELEASE``). Off-device (the test
48
48
  environment), returns the host Python's version so user code can
49
49
  still introspect *something*.
50
50
  """
51
- if IS_IOS:
51
+ if IS_IOS or IS_ANDROID:
52
52
  try:
53
- from rubicon.objc import ObjCClass
53
+ from .native_modules.registry import native_module
54
54
 
55
- UIDevice = ObjCClass("UIDevice")
56
- return str(UIDevice.currentDevice.systemVersion)
55
+ info = native_module("Device").call("info")
56
+ if isinstance(info, dict) and info.get("os_version"):
57
+ return str(info["os_version"])
57
58
  except Exception:
58
59
  pass
59
60
  sim_version = os.environ.get("SIMULATOR_RUNTIME_VERSION")
60
61
  if sim_version:
61
62
  return sim_version
62
- if IS_ANDROID:
63
- try:
64
- from java import jclass
65
-
66
- Build = jclass("android.os.Build$VERSION")
67
- return str(Build.RELEASE)
68
- except Exception:
69
- pass
70
63
  return f"python-{sys.version_info.major}.{sys.version_info.minor}"
71
64
 
72
65
 
66
+ class _LazyVersion:
67
+ """Resolve ``Platform.Version`` on first access (it needs the bridge on device)."""
68
+
69
+ _value: Optional[str] = None
70
+
71
+ def __get__(self, obj: object, owner: Optional[type] = None) -> str:
72
+ if _LazyVersion._value is None:
73
+ _LazyVersion._value = _detect_version()
74
+ return _LazyVersion._value
75
+
76
+
73
77
  class Platform:
74
78
  """Platform-aware constants and the ``select`` dispatcher.
75
79
 
@@ -82,7 +86,7 @@ class Platform:
82
86
  OS: str = _detect_os()
83
87
  """``"ios"``, ``"android"``, ``"desktop"``, or ``"test"``."""
84
88
 
85
- Version: str = _detect_version()
89
+ Version: str = _LazyVersion() # type: ignore[assignment]
86
90
  """Best-effort OS version string (``"17.4"``, ``"14"``, ``"python-3.11"``)."""
87
91
 
88
92
  is_ios: bool = IS_IOS
@@ -245,8 +245,8 @@ def ios_tab_bar_height() -> float:
245
245
  deliberately extends the root view past the bottom safe area for
246
246
  this very reason; the tab bar absorbs the inset and UIKit
247
247
  renders the pill with internal padding for the home indicator.
248
- Used by ``pythonnative.native_views.ios.TabBarHandler``; exposed
249
- here so the formula is testable without importing the iOS
250
- handler module (which requires ``rubicon-objc``).
248
+ The Swift ``TabBarManager`` applies the same formula natively;
249
+ this Python copy keeps the layout engine's expectations testable
250
+ off device.
251
251
  """
252
252
  return IOS_TAB_BAR_BASE_HEIGHT_PT + get_safe_area_insets().bottom
@@ -512,9 +512,6 @@ def collect_logcat_filters() -> List[str]:
512
512
  return [
513
513
  "python.stdout:V",
514
514
  "python.stderr:V",
515
- "MainActivity:V",
516
- "ScreenFragment:V",
517
- "Navigator:V",
518
515
  "PythonNative:V",
519
516
  "AndroidRuntime:E",
520
517
  "System.err:W",
@@ -28,6 +28,7 @@ from __future__ import annotations
28
28
  import compileall
29
29
  import os
30
30
  import platform as platform_module
31
+ import shutil
31
32
  import subprocess
32
33
  import sys
33
34
  from dataclasses import dataclass, field
@@ -37,6 +38,7 @@ from typing import Callable, List, Optional, Sequence, Union
37
38
 
38
39
  from . import android as android_config
39
40
  from . import ios as ios_config
41
+ from . import plugins as native_plugins
40
42
  from . import runtime_assets
41
43
  from .android import AndroidLayout
42
44
  from .config import AppConfig
@@ -177,6 +179,20 @@ class BuildArtifacts:
177
179
 
178
180
  _TEMPLATE_NAMES = {"android": "android_template", "ios": "ios_template"}
179
181
 
182
+ # A source checkout accumulates Gradle, SwiftPM, and Xcode outputs next to
183
+ # the native library sources; a staged project must start from a clean tree.
184
+ _TEMPLATE_IGNORE = shutil.ignore_patterns(
185
+ "build",
186
+ ".build",
187
+ ".gradle",
188
+ ".swiftpm",
189
+ "xcuserdata",
190
+ "DerivedData",
191
+ "__pycache__",
192
+ "*.pyc",
193
+ ".DS_Store",
194
+ )
195
+
180
196
 
181
197
  def stage_template(template_name: str, destination: Path) -> Path:
182
198
  """Copy a bundled native template into ``destination``.
@@ -196,22 +212,20 @@ def stage_template(template_name: str, destination: Path) -> Path:
196
212
  Raises:
197
213
  BuildError: If no bundled copy can be located.
198
214
  """
199
- import shutil
200
-
201
215
  dest_path = destination / template_name
202
216
  destination.mkdir(parents=True, exist_ok=True)
203
217
 
204
218
  # Dev-first: local source package templates.
205
219
  local = Path(__file__).resolve().parents[1] / "templates" / template_name
206
220
  if local.is_dir():
207
- shutil.copytree(local, dest_path, dirs_exist_ok=True)
221
+ shutil.copytree(local, dest_path, dirs_exist_ok=True, ignore=_TEMPLATE_IGNORE)
208
222
  return dest_path
209
223
 
210
224
  try:
211
225
  candidate = resources.files("pythonnative").joinpath("templates").joinpath(template_name)
212
226
  with resources.as_file(candidate) as resolved:
213
227
  if Path(resolved).is_dir():
214
- shutil.copytree(resolved, dest_path, dirs_exist_ok=True)
228
+ shutil.copytree(resolved, dest_path, dirs_exist_ok=True, ignore=_TEMPLATE_IGNORE)
215
229
  return dest_path
216
230
  except (ModuleNotFoundError, FileNotFoundError, OSError):
217
231
  pass
@@ -280,6 +294,7 @@ class Builder:
280
294
  build_dir = self.build_root / platform
281
295
  build_dir.mkdir(parents=True, exist_ok=True)
282
296
  project_dir = stage_template(_TEMPLATE_NAMES[platform], build_dir)
297
+ plugins = self._discover_plugins()
283
298
 
284
299
  if platform == "android":
285
300
  layout = android_config.configure(
@@ -288,6 +303,7 @@ class Builder:
288
303
  dev_lib_root=self.dev_lib_root,
289
304
  log=self.log,
290
305
  )
306
+ native_plugins.stage_android_plugins(project_dir, plugins, log=self.log)
291
307
  return PreparedProject(
292
308
  platform=platform,
293
309
  build_dir=build_dir,
@@ -297,6 +313,7 @@ class Builder:
297
313
  )
298
314
 
299
315
  ios_layout = ios_config.configure(project_dir, self.config, log=self.log)
316
+ native_plugins.stage_ios_plugins(project_dir, plugins, log=self.log)
300
317
  self._stage_ios_python(project_dir, release=release)
301
318
  self._link_ios_runtime(project_dir)
302
319
  return PreparedProject(
@@ -307,6 +324,20 @@ class Builder:
307
324
  ios=ios_layout,
308
325
  )
309
326
 
327
+ def _discover_plugins(self) -> List[native_plugins.NativePlugin]:
328
+ """Collect native plugins from entry points and ``[plugins].paths``.
329
+
330
+ Raises:
331
+ BuildError: If a project-local plugin directory is invalid.
332
+ """
333
+ try:
334
+ return native_plugins.discover_plugins(
335
+ extra_paths=[self.config.resolve_path(p) for p in self.config.plugin_paths],
336
+ log=self.log,
337
+ )
338
+ except native_plugins.PluginError as exc:
339
+ raise BuildError(f"Invalid native plugin: {exc}") from exc
340
+
310
341
  def _stage_ios_python(self, project_dir: Path, *, release: bool) -> None:
311
342
  """Stage ``app/`` and ``app_packages/`` at the Xcode project root.
312
343
 
@@ -314,8 +345,6 @@ class Builder:
314
345
  and the "Install Python runtime" build phase converts any
315
346
  binary modules inside them into signed frameworks.
316
347
  """
317
- import shutil
318
-
319
348
  app_dir = project_dir / "app"
320
349
  packages_dir = project_dir / "app_packages"
321
350
  for directory in (app_dir, packages_dir):
@@ -336,14 +365,9 @@ class Builder:
336
365
  ignore=android_config.LIB_IGNORE,
337
366
  )
338
367
 
339
- # rubicon-objc supplies the iOS Objective-C bridge; user
340
- # requirements ride along in the same site directory.
341
- result = self.runner.run(
342
- [sys.executable, "-m", "pip", "install", "--no-deps", "--upgrade", "rubicon-objc", "-t", str(packages_dir)],
343
- capture=True,
344
- )
345
- if not result.ok:
346
- raise BuildError(f"pip install rubicon-objc failed:\n{result.stderr}")
368
+ # The framework has no Python-side native dependencies (the
369
+ # bridge into PythonNativeKit is ctypes); only user requirements
370
+ # are installed into the site directory.
347
371
  if self.config.requirements:
348
372
  result = self.runner.run(
349
373
  [sys.executable, "-m", "pip", "install", "-t", str(packages_dir), *self.config.requirements],
@@ -389,8 +413,6 @@ class Builder:
389
413
  framework is hundreds of megabytes); staging falls back to a
390
414
  copy when symlinks are unavailable.
391
415
  """
392
- import shutil
393
-
394
416
  runtime = self._ios_runtime()
395
417
  link = project_dir / "Python.xcframework"
396
418
  if link.is_symlink() or link.is_file():
@@ -34,6 +34,9 @@ splash = "assets/splash.png" # splash/launch image
34
34
  [requirements]
35
35
  packages = ["humanize", "httpx"]
36
36
 
37
+ [plugins]
38
+ paths = ["native/my_plugin"] # local native plugins (pn_plugin.json + ios/ + android/)
39
+
37
40
  [ios]
38
41
  deployment_target = "13.0"
39
42
  development_team = "ABCDE12345"
@@ -235,6 +238,9 @@ class AppConfig:
235
238
  icon: Optional source icon path (``[assets].icon``).
236
239
  splash: Optional splash image path (``[assets].splash``).
237
240
  requirements: Third-party pip packages (``[requirements].packages``).
241
+ plugin_paths: Project-local native plugin directories
242
+ (``[plugins].paths``); installed packages contribute plugins
243
+ through the ``pythonnative.plugins`` entry point group instead.
238
244
  ios: Nested [`IOSConfig`][pythonnative.project.config.IOSConfig].
239
245
  android: Nested
240
246
  [`AndroidConfig`][pythonnative.project.config.AndroidConfig].
@@ -254,6 +260,7 @@ class AppConfig:
254
260
  icon: Optional[str] = None
255
261
  splash: Optional[str] = None
256
262
  requirements: List[str] = field(default_factory=list)
263
+ plugin_paths: List[str] = field(default_factory=list)
257
264
  ios: IOSConfig = field(default_factory=IOSConfig)
258
265
  android: AndroidConfig = field(default_factory=AndroidConfig)
259
266
  project_root: Path = field(default_factory=Path.cwd)
@@ -345,8 +352,8 @@ class AppConfig:
345
352
 
346
353
  Args:
347
354
  data: A parsed TOML mapping (top-level tables: ``app``,
348
- ``permissions``, ``assets``, ``requirements``, ``ios``,
349
- ``android``).
355
+ ``permissions``, ``assets``, ``requirements``, ``plugins``,
356
+ ``ios``, ``android``).
350
357
  project_root: Directory the config came from, used to resolve
351
358
  relative paths.
352
359
 
@@ -382,6 +389,9 @@ class AppConfig:
382
389
  requirements = _expect_table(data, "requirements", optional=True)
383
390
  config.requirements = _opt_str_list(requirements, "packages")
384
391
 
392
+ plugins = _expect_table(data, "plugins", optional=True)
393
+ config.plugin_paths = _opt_str_list(plugins, "paths")
394
+
385
395
  config.validate()
386
396
  return config
387
397