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
@@ -0,0 +1,513 @@
1
+ """Native module registry: name -> callable module, on device or off.
2
+
3
+ A *native module* is a named bag of methods implemented in Swift and
4
+ Kotlin (``Camera``, ``Storage``, ``Haptics``, ...). Python facades in
5
+ this package never touch platform APIs; they obtain a
6
+ [`NativeModule`][pythonnative.native_modules.registry.NativeModule]
7
+ through [`native_module`][pythonnative.native_modules.registry.native_module]
8
+ and call methods on it:
9
+
10
+ ```python
11
+ _clipboard = native_module("Clipboard")
12
+ _clipboard.call("set_string", text="hello")
13
+ text = _clipboard.call("get_string")
14
+ result = await _camera.call_async("take_photo")
15
+ ```
16
+
17
+ On device the module is a
18
+ [`BridgeModule`][pythonnative.native_modules.registry.BridgeModule]
19
+ that speaks the ``call(module, method, args_json)`` protocol described
20
+ in ``docs/concepts/bridge.md``. Off device (tests, ``pn preview``) the
21
+ same name resolves to a
22
+ [`PythonModule`][pythonnative.native_modules.registry.PythonModule]
23
+ wrapping a plain Python object with the same method names; the
24
+ built-in fallbacks live in ``pythonnative.native_modules.desktop`` and
25
+ third-party packages register theirs through the
26
+ ``pythonnative.modules`` entry point group or
27
+ [`register_python_module`][pythonnative.native_modules.registry.register_python_module].
28
+
29
+ Modules can also push events (``AppState`` changes, deep links,
30
+ battery updates). Facades subscribe with
31
+ [`NativeModule.add_listener`][pythonnative.native_modules.registry.NativeModule.add_listener];
32
+ native delivers through
33
+ [`dispatch_module_message`][pythonnative.native_modules.registry.dispatch_module_message]
34
+ and Python implementations through
35
+ [`emit`][pythonnative.native_modules.registry.emit].
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import asyncio
41
+ import inspect
42
+ import itertools
43
+ import threading
44
+ from typing import Any, Callable, Dict, List, Optional
45
+
46
+ from ..bridge import codec
47
+
48
+ __all__ = [
49
+ "ENTRY_POINT_GROUP",
50
+ "BridgeModule",
51
+ "NativeModule",
52
+ "NativeModuleError",
53
+ "PythonModule",
54
+ "dispatch_module_message",
55
+ "emit",
56
+ "native_module",
57
+ "on_event",
58
+ "register_python_module",
59
+ "unregister_python_module",
60
+ ]
61
+
62
+ ENTRY_POINT_GROUP = "pythonnative.modules"
63
+ """Entry-point group for Python (desktop / test) implementations of native modules."""
64
+
65
+ Listener = Callable[[Any], None]
66
+
67
+
68
+ class NativeModuleError(RuntimeError):
69
+ """A native module method failed.
70
+
71
+ Attributes:
72
+ module: Module name.
73
+ method: Method name.
74
+ code: Optional machine-readable code supplied by native.
75
+ """
76
+
77
+ def __init__(self, module: str, method: str, message: str, code: Optional[str] = None) -> None:
78
+ super().__init__(f"{module}.{method}: {message}")
79
+ self.module = module
80
+ self.method = method
81
+ self.message = message
82
+ self.code = code
83
+
84
+
85
+ class NativeModule:
86
+ """Common surface of bridge-backed and Python-backed modules."""
87
+
88
+ name: str
89
+
90
+ def __init__(self, name: str) -> None:
91
+ self.name = name
92
+ self._listeners: Dict[str, List[Listener]] = {}
93
+ self._lock = threading.Lock()
94
+
95
+ def call(self, method: str, **args: Any) -> Any:
96
+ """Invoke ``method`` synchronously and return its value.
97
+
98
+ Raises:
99
+ NativeModuleError: When the native side reports a failure.
100
+ RuntimeError: When the method only completes asynchronously
101
+ (use [`call_async`][pythonnative.native_modules.registry.NativeModule.call_async]).
102
+ """
103
+ raise NotImplementedError
104
+
105
+ async def call_async(self, method: str, **args: Any) -> Any:
106
+ """Invoke ``method`` and await its result."""
107
+ raise NotImplementedError
108
+
109
+ def add_listener(self, event: str, callback: Listener) -> Callable[[], None]:
110
+ """Subscribe to ``event``; returns an unsubscribe callable."""
111
+ with self._lock:
112
+ self._listeners.setdefault(event, []).append(callback)
113
+
114
+ def _remove() -> None:
115
+ with self._lock:
116
+ bucket = self._listeners.get(event)
117
+ if bucket is None:
118
+ return
119
+ try:
120
+ bucket.remove(callback)
121
+ except ValueError:
122
+ pass
123
+ if not bucket:
124
+ self._listeners.pop(event, None)
125
+
126
+ return _remove
127
+
128
+ def listener_count(self, event: Optional[str] = None) -> int:
129
+ """Number of listeners for ``event`` (or for every event when ``None``)."""
130
+ with self._lock:
131
+ if event is None:
132
+ return sum(len(b) for b in self._listeners.values())
133
+ return len(self._listeners.get(event, ()))
134
+
135
+ def _deliver(self, event: str, payload: Any) -> None:
136
+ with self._lock:
137
+ callbacks = list(self._listeners.get(event, ()))
138
+ for callback in callbacks:
139
+ try:
140
+ callback(payload)
141
+ except Exception:
142
+ from .. import diagnostics
143
+
144
+ diagnostics.swallowed(f"native_modules.{self.name}.{event}")
145
+
146
+
147
+ # ======================================================================
148
+ # Bridge-backed modules (device)
149
+ # ======================================================================
150
+
151
+ _call_ids = itertools.count(1)
152
+ _pending: Dict[int, "asyncio.Future[Any]"] = {}
153
+ _pending_meta: Dict[int, tuple] = {}
154
+ _pending_lock = threading.Lock()
155
+
156
+
157
+ class BridgeModule(NativeModule):
158
+ """A module implemented natively; every call crosses the bridge once."""
159
+
160
+ def __init__(self, name: str, transport: Any = None) -> None:
161
+ super().__init__(name)
162
+ self._transport = transport
163
+
164
+ @property
165
+ def transport(self) -> Any:
166
+ """The transport in use (resolved lazily on first access)."""
167
+ if self._transport is None:
168
+ from ..bridge import get_transport
169
+
170
+ self._transport = get_transport()
171
+ return self._transport
172
+
173
+ def _invoke(self, method: str, args: Dict[str, Any], call_id: int) -> Any:
174
+ envelope = codec.dumps({"call_id": call_id, "args": codec.to_jsonable(args)})
175
+ raw = self.transport.call(self.name, method, envelope)
176
+ result = codec.loads(raw)
177
+ if result is None:
178
+ return {"ok": True, "value": None}
179
+ if not isinstance(result, dict):
180
+ return {"ok": True, "value": result}
181
+ return result
182
+
183
+ def call(self, method: str, **args: Any) -> Any:
184
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
185
+ result = self._invoke(method, args, 0)
186
+ if result.get("pending"):
187
+ raise RuntimeError(f"{self.name}.{method} completes asynchronously; use call_async()")
188
+ if result.get("ok", True):
189
+ return result.get("value")
190
+ raise NativeModuleError(self.name, method, str(result.get("error", "unknown error")), result.get("code"))
191
+
192
+ async def call_async(self, method: str, **args: Any) -> Any:
193
+ """Invoke ``method`` and await its result."""
194
+ loop = asyncio.get_running_loop()
195
+ call_id = next(_call_ids)
196
+ future: asyncio.Future[Any] = loop.create_future()
197
+ with _pending_lock:
198
+ _pending[call_id] = future
199
+ _pending_meta[call_id] = (self.name, method)
200
+ try:
201
+ result = self._invoke(method, args, call_id)
202
+ except Exception:
203
+ with _pending_lock:
204
+ _pending.pop(call_id, None)
205
+ _pending_meta.pop(call_id, None)
206
+ raise
207
+ if not result.get("pending"):
208
+ with _pending_lock:
209
+ _pending.pop(call_id, None)
210
+ _pending_meta.pop(call_id, None)
211
+ if result.get("ok", True):
212
+ return result.get("value")
213
+ raise NativeModuleError(self.name, method, str(result.get("error", "unknown error")), result.get("code"))
214
+ return await future
215
+
216
+
217
+ def _settle(call_id: int, message: Dict[str, Any]) -> None:
218
+ with _pending_lock:
219
+ future = _pending.pop(call_id, None)
220
+ meta = _pending_meta.pop(call_id, ("?", "?"))
221
+ if future is None:
222
+ return
223
+ from ..runtime import reject_future, resolve_future
224
+
225
+ if message.get("ok", True):
226
+ resolve_future(future, message.get("value"))
227
+ else:
228
+ module, method = meta
229
+ reject_future(
230
+ future, NativeModuleError(module, method, str(message.get("error", "unknown error")), message.get("code"))
231
+ )
232
+
233
+
234
+ # ======================================================================
235
+ # Python-backed modules (desktop / tests)
236
+ # ======================================================================
237
+
238
+
239
+ class PythonModule(NativeModule):
240
+ """A module implemented by a plain Python object.
241
+
242
+ Methods are looked up by name on ``impl``; keyword arguments are
243
+ forwarded. A method may be a coroutine function (or return an
244
+ awaitable), in which case ``call`` raises and ``call_async`` awaits
245
+ it. The implementation can push events with
246
+ [`emit`][pythonnative.native_modules.registry.emit].
247
+ """
248
+
249
+ def __init__(self, name: str, impl: Any = None) -> None:
250
+ super().__init__(name)
251
+ self._impl = impl
252
+
253
+ @property
254
+ def impl(self) -> Any:
255
+ """The implementation object, resolved on first use.
256
+
257
+ Facades call ``native_module()`` at import time, so resolution is
258
+ deferred until the first method call; by then the package's
259
+ desktop implementation (or entry point) has had a chance to
260
+ register.
261
+
262
+ Raises:
263
+ KeyError: If no implementation is registered for the module.
264
+ """
265
+ if self._impl is None:
266
+ self._impl = _resolve_python_impl(self.name)
267
+ return self._impl
268
+
269
+ def _method(self, method: str) -> Callable[..., Any]:
270
+ fn = getattr(self.impl, method, None)
271
+ if fn is None or not callable(fn):
272
+ raise NativeModuleError(self.name, method, "unknown method", code="unknown_method")
273
+ return fn
274
+
275
+ def call(self, method: str, **args: Any) -> Any:
276
+ """Call a native module method with a ``{"call_id", "args"}`` envelope."""
277
+ fn = self._method(method)
278
+ if inspect.iscoroutinefunction(fn):
279
+ raise RuntimeError(f"{self.name}.{method} is asynchronous; use call_async()")
280
+ try:
281
+ value = fn(**args)
282
+ except NativeModuleError:
283
+ raise
284
+ except Exception as exc:
285
+ raise NativeModuleError(self.name, method, str(exc)) from exc
286
+ if inspect.isawaitable(value):
287
+ raise RuntimeError(f"{self.name}.{method} returned an awaitable; use call_async()")
288
+ return value
289
+
290
+ async def call_async(self, method: str, **args: Any) -> Any:
291
+ """Invoke ``method`` and await its result."""
292
+ fn = self._method(method)
293
+ try:
294
+ value = fn(**args)
295
+ if inspect.isawaitable(value):
296
+ value = await value
297
+ except NativeModuleError:
298
+ raise
299
+ except Exception as exc:
300
+ raise NativeModuleError(self.name, method, str(exc)) from exc
301
+ return value
302
+
303
+
304
+ # ======================================================================
305
+ # Registry
306
+ # ======================================================================
307
+
308
+ _modules: Dict[str, NativeModule] = {}
309
+ _python_impls: Dict[str, Any] = {}
310
+ _registry_lock = threading.Lock()
311
+ _discovered = False
312
+
313
+
314
+ def register_python_module(name: str, impl: Any) -> None:
315
+ """Register ``impl`` as the off-device implementation of module ``name``.
316
+
317
+ ``impl`` is an object (or a zero-arg factory returning one) whose
318
+ methods match the module's method names. Registering replaces any
319
+ earlier implementation and invalidates the cached module so the
320
+ next [`native_module`][pythonnative.native_modules.registry.native_module]
321
+ call picks it up.
322
+ """
323
+ with _registry_lock:
324
+ _python_impls[name] = impl
325
+ _modules.pop(name, None)
326
+
327
+
328
+ def unregister_python_module(name: str) -> None:
329
+ """Remove a Python implementation registered for ``name`` (tests)."""
330
+ with _registry_lock:
331
+ _python_impls.pop(name, None)
332
+ _modules.pop(name, None)
333
+
334
+
335
+ def native_module(name: str) -> NativeModule:
336
+ """Return the module registered under ``name`` for the current platform.
337
+
338
+ On iOS and Android this is always a
339
+ [`BridgeModule`][pythonnative.native_modules.registry.BridgeModule];
340
+ the native runtime decides whether the module exists when a method
341
+ is first called. Elsewhere it is the registered
342
+ [`PythonModule`][pythonnative.native_modules.registry.PythonModule].
343
+
344
+ Off device the implementation is resolved lazily, on the first
345
+ method call, so facades can call this at import time; a missing
346
+ implementation surfaces then as ``KeyError``.
347
+ """
348
+ module = _modules.get(name)
349
+ if module is not None:
350
+ return module
351
+ with _registry_lock:
352
+ module = _modules.get(name)
353
+ if module is not None:
354
+ return module
355
+ module = _create(name)
356
+ _modules[name] = module
357
+ return module
358
+
359
+
360
+ def _create(name: str) -> NativeModule:
361
+ from ..bridge import has_transport
362
+
363
+ if has_transport():
364
+ return BridgeModule(name)
365
+ return PythonModule(name)
366
+
367
+
368
+ def _resolve_python_impl(name: str) -> Any:
369
+ _discover_entry_points()
370
+ with _registry_lock:
371
+ impl = _python_impls.get(name)
372
+ if impl is None:
373
+ from . import desktop
374
+
375
+ impl = desktop.default_implementation(name)
376
+ if impl is None:
377
+ raise KeyError(
378
+ f"No native module named {name!r} is available off device; register one with register_python_module()"
379
+ )
380
+ if inspect.isclass(impl) or inspect.isfunction(impl):
381
+ impl = impl()
382
+ return impl
383
+
384
+
385
+ def _discover_entry_points() -> None:
386
+ global _discovered
387
+ if _discovered:
388
+ return
389
+ _discovered = True
390
+ try:
391
+ from importlib.metadata import entry_points
392
+ except ImportError: # pragma: no cover
393
+ return
394
+ try:
395
+ eps = entry_points()
396
+ except Exception: # pragma: no cover
397
+ return
398
+ selected: List[Any] = []
399
+ if hasattr(eps, "select"):
400
+ try:
401
+ selected = list(eps.select(group=ENTRY_POINT_GROUP))
402
+ except Exception:
403
+ selected = []
404
+ if not selected:
405
+ getter = getattr(eps, "get", None)
406
+ if getter is not None:
407
+ try:
408
+ selected = list(getter(ENTRY_POINT_GROUP, []))
409
+ except Exception:
410
+ selected = []
411
+ for ep in selected:
412
+ try:
413
+ loaded = ep.load()
414
+ except Exception as exc: # pragma: no cover - defensive
415
+ import sys
416
+
417
+ print(
418
+ f"[pythonnative.native_modules] Failed to load module entry point {ep.name!r}: {exc!r}", file=sys.stderr
419
+ )
420
+ continue
421
+ module_name = getattr(loaded, "name", None) or ep.name
422
+ if inspect.isfunction(loaded):
423
+ try:
424
+ loaded()
425
+ except Exception as exc: # pragma: no cover - defensive
426
+ import sys
427
+
428
+ print(f"[pythonnative.native_modules] entry point {ep.name!r} raised: {exc!r}", file=sys.stderr)
429
+ continue
430
+ _python_impls.setdefault(str(module_name), loaded)
431
+
432
+
433
+ # ======================================================================
434
+ # Inbound messages and events
435
+ # ======================================================================
436
+
437
+
438
+ def dispatch_module_message(module: str, message: Dict[str, Any]) -> None:
439
+ """Route a native ``callback("module", ...)`` payload.
440
+
441
+ ``{"call_id": n, "ok": ..., "value"|"error": ...}`` settles a
442
+ pending ``call_async``; ``{"event": name, "payload": ...}`` fans
443
+ out to listeners.
444
+ """
445
+ if not isinstance(message, dict):
446
+ return
447
+ if "call_id" in message:
448
+ try:
449
+ _settle(int(message["call_id"]), message)
450
+ except (TypeError, ValueError):
451
+ pass
452
+ return
453
+ event = message.get("event")
454
+ if event:
455
+ emit(module, str(event), message.get("payload"))
456
+
457
+
458
+ _hooks: Dict[tuple, List[Listener]] = {}
459
+
460
+
461
+ def on_event(module: str, event: str, callback: Listener) -> Callable[[], None]:
462
+ """Subscribe to ``module``/``event`` without resolving the module.
463
+
464
+ Facades use this at import time to route native pushes (``AppState``
465
+ ``change``, ``Linking`` ``url``, ...) into their own listener lists.
466
+ Unlike [`NativeModule.add_listener`][pythonnative.native_modules.registry.NativeModule.add_listener]
467
+ the subscription survives module re-creation and never touches the
468
+ platform. Returns an unsubscribe callable.
469
+ """
470
+ key = (module, event)
471
+ with _registry_lock:
472
+ _hooks.setdefault(key, []).append(callback)
473
+
474
+ def _remove() -> None:
475
+ with _registry_lock:
476
+ bucket = _hooks.get(key)
477
+ if bucket and callback in bucket:
478
+ bucket.remove(callback)
479
+
480
+ return _remove
481
+
482
+
483
+ def emit(module: str, event: str, payload: Any = None) -> None:
484
+ """Deliver ``event`` to every listener of ``module`` (any platform).
485
+
486
+ Python implementations use this to behave like their native
487
+ counterparts (a test can emit ``AppState`` ``change`` events, for
488
+ example).
489
+ """
490
+ with _registry_lock:
491
+ hooks = list(_hooks.get((module, event), ()))
492
+ for hook in hooks:
493
+ try:
494
+ hook(payload)
495
+ except Exception:
496
+ from .. import diagnostics
497
+
498
+ diagnostics.swallowed(f"native_modules.{module}.{event}")
499
+ target = _modules.get(module)
500
+ if target is not None:
501
+ target._deliver(event, payload)
502
+
503
+
504
+ def _reset_for_tests() -> None:
505
+ """Forget cached modules and pending calls (test isolation)."""
506
+ global _discovered
507
+ with _registry_lock:
508
+ _modules.clear()
509
+ _python_impls.clear()
510
+ _discovered = False
511
+ with _pending_lock:
512
+ _pending.clear()
513
+ _pending_meta.clear()