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,322 @@
1
+ """Off-device implementations of the built-in native modules.
2
+
3
+ On iOS and Android every module in this package is implemented in
4
+ Swift and Kotlin (``PythonNativeKit`` and the ``pythonnative`` Gradle
5
+ module). Off device (``pn preview``, unit tests) the same module names
6
+ resolve to the plain Python classes below, which keep the API usable
7
+ without a device: in-memory buffers, ``"unknown"`` states, and no-op
8
+ feedback.
9
+
10
+ Each class has the same method names and argument shapes as its native
11
+ counterpart, so a facade in this package never branches on platform.
12
+ Apps and tests may swap any of these with
13
+ [`register_python_module`][pythonnative.native_modules.registry.register_python_module].
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import platform as _platform
21
+ import sys
22
+ import threading
23
+ from typing import Any, Callable, Dict, List, Optional
24
+
25
+ __all__ = ["default_implementation"]
26
+
27
+
28
+ # ======================================================================
29
+ # Device / host information
30
+ # ======================================================================
31
+
32
+
33
+ class DesktopDevice:
34
+ """Static device information for the desktop preview."""
35
+
36
+ def info(self) -> Dict[str, Any]:
37
+ home = os.path.expanduser("~")
38
+ app_dir = os.path.join(home, ".pythonnative_data")
39
+ return {
40
+ "platform": "desktop",
41
+ "os": sys.platform,
42
+ "os_version": _platform.release(),
43
+ "model": _platform.machine(),
44
+ "app_dir": app_dir,
45
+ "cache_dir": os.path.join(app_dir, "cache"),
46
+ "temp_dir": os.path.join(app_dir, "tmp"),
47
+ "locale": os.environ.get("LANG", "en_US").split(".")[0],
48
+ "app_version": "0.0.0",
49
+ "build_number": "0",
50
+ "bundle_id": "com.pythonnative.preview",
51
+ "python_version": _platform.python_version(),
52
+ }
53
+
54
+
55
+ class DesktopAppState:
56
+ def current_state(self) -> str:
57
+ return "active"
58
+
59
+
60
+ # ======================================================================
61
+ # Storage
62
+ # ======================================================================
63
+
64
+
65
+ class DesktopStorage:
66
+ """Dict-backed ``AsyncStorage`` with optional JSON persistence.
67
+
68
+ Set ``PN_STORAGE_DIR`` to persist between runs (``pn preview`` does
69
+ this); leave it unset in tests for a purely in-memory store.
70
+ """
71
+
72
+ def __init__(self) -> None:
73
+ self._lock = threading.Lock()
74
+ self._store: Dict[str, str] = {}
75
+ self._loaded = False
76
+
77
+ def _path(self) -> Optional[str]:
78
+ base = os.environ.get("PN_STORAGE_DIR")
79
+ if not base:
80
+ return None
81
+ try:
82
+ os.makedirs(base, exist_ok=True)
83
+ except OSError:
84
+ return None
85
+ return os.path.join(base, "pn_async_storage.json")
86
+
87
+ def _load(self) -> None:
88
+ if self._loaded:
89
+ return
90
+ self._loaded = True
91
+ path = self._path()
92
+ if path is None or not os.path.exists(path):
93
+ return
94
+ try:
95
+ with open(path, encoding="utf-8") as f:
96
+ data = json.load(f)
97
+ except (OSError, json.JSONDecodeError):
98
+ return
99
+ if isinstance(data, dict):
100
+ self._store.update({str(k): str(v) for k, v in data.items()})
101
+
102
+ def _persist(self) -> None:
103
+ path = self._path()
104
+ if path is None:
105
+ return
106
+ try:
107
+ with open(path, "w", encoding="utf-8") as f:
108
+ json.dump(self._store, f)
109
+ except OSError:
110
+ pass
111
+
112
+ def get(self, key: str) -> Optional[str]:
113
+ with self._lock:
114
+ self._load()
115
+ return self._store.get(key)
116
+
117
+ def set(self, key: str, value: str) -> None:
118
+ with self._lock:
119
+ self._load()
120
+ self._store[key] = value
121
+ self._persist()
122
+
123
+ def delete(self, key: str) -> None:
124
+ with self._lock:
125
+ self._load()
126
+ self._store.pop(key, None)
127
+ self._persist()
128
+
129
+ def all_keys(self) -> List[str]:
130
+ with self._lock:
131
+ self._load()
132
+ return list(self._store.keys())
133
+
134
+ def clear(self) -> None:
135
+ with self._lock:
136
+ self._store.clear()
137
+ self._persist()
138
+
139
+ def _reset(self) -> None:
140
+ """Forget everything, including the on-disk snapshot (tests)."""
141
+ with self._lock:
142
+ self._store.clear()
143
+ self._loaded = False
144
+
145
+
146
+ class DesktopSecureStore:
147
+ def __init__(self) -> None:
148
+ self._store: Dict[str, str] = {}
149
+
150
+ def set_item(self, key: str, value: str) -> bool:
151
+ self._store[key] = value
152
+ return True
153
+
154
+ def get_item(self, key: str) -> Optional[str]:
155
+ return self._store.get(key)
156
+
157
+ def delete_item(self, key: str) -> bool:
158
+ return self._store.pop(key, None) is not None
159
+
160
+ def clear(self) -> None:
161
+ self._store.clear()
162
+
163
+
164
+ class DesktopClipboard:
165
+ def __init__(self) -> None:
166
+ self._buffer = ""
167
+
168
+ def set_string(self, text: str) -> None:
169
+ self._buffer = "" if text is None else str(text)
170
+
171
+ def get_string(self) -> str:
172
+ return self._buffer
173
+
174
+
175
+ # ======================================================================
176
+ # System integration
177
+ # ======================================================================
178
+
179
+
180
+ class DesktopAlert:
181
+ """Records alerts and answers with scripted responses.
182
+
183
+ The log and response queue live on
184
+ [`Alert`][pythonnative.alerts.Alert] (``Alert._test_log`` and
185
+ ``Alert.set_test_response``) so tests have one place to look.
186
+ """
187
+
188
+ @staticmethod
189
+ def _record(title: str, message: Optional[str], buttons: List[Dict[str, Any]], style: str) -> Any:
190
+ from ..alerts import Alert
191
+
192
+ Alert._test_log.append({"title": title, "message": message, "buttons": list(buttons), "style": style})
193
+ return Alert
194
+
195
+ def show(self, title: str, message: Optional[str], buttons: List[Dict[str, Any]], style: str) -> None:
196
+ self._record(title, message, buttons, style)
197
+
198
+ def present(self, title: str, message: Optional[str], buttons: List[Dict[str, Any]], style: str) -> int:
199
+ return int(self._record(title, message, buttons, style)._next_test_response())
200
+
201
+
202
+ class DesktopShare:
203
+ def share(self, message: Optional[str] = None, url: Optional[str] = None, title: Optional[str] = None) -> bool:
204
+ del message, url, title
205
+ return False
206
+
207
+
208
+ class DesktopLinking:
209
+ def open_url(self, url: str) -> bool:
210
+ del url
211
+ return False
212
+
213
+ def can_open_url(self, url: str) -> bool:
214
+ del url
215
+ return False
216
+
217
+ def open_settings(self) -> bool:
218
+ return False
219
+
220
+
221
+ class DesktopHaptics:
222
+ def impact(self, style: str = "medium") -> None:
223
+ del style
224
+
225
+ def notification(self, type: str = "success") -> None: # noqa: A002 - wire name
226
+ del type
227
+
228
+ def selection(self) -> None:
229
+ pass
230
+
231
+ def vibrate(self, duration_ms: int = 400) -> None:
232
+ del duration_ms
233
+
234
+ def cancel(self) -> None:
235
+ pass
236
+
237
+
238
+ class DesktopBattery:
239
+ def get_level(self) -> float:
240
+ return -1.0
241
+
242
+ def get_state(self) -> str:
243
+ return "unknown"
244
+
245
+
246
+ class DesktopNetInfo:
247
+ def fetch(self) -> Optional[Dict[str, Any]]:
248
+ # ``None`` means "no fresh reading"; the facade keeps whatever
249
+ # snapshot was last dispatched (tests push those directly).
250
+ return None
251
+
252
+
253
+ class DesktopPermissions:
254
+ def check(self, permission: str) -> str:
255
+ del permission
256
+ return "undetermined"
257
+
258
+ def request(self, permission: str) -> str:
259
+ del permission
260
+ return "undetermined"
261
+
262
+
263
+ class DesktopNotifications:
264
+ def request_permission(self) -> bool:
265
+ return False
266
+
267
+ def schedule(self, title: str, body: str = "", delay_seconds: float = 0, identifier: str = "default") -> bool:
268
+ del title, body, delay_seconds, identifier
269
+ return False
270
+
271
+ def cancel(self, identifier: str = "default") -> None:
272
+ del identifier
273
+
274
+ def get_device_token(self) -> Optional[str]:
275
+ return None
276
+
277
+
278
+ class DesktopCamera:
279
+ def take_photo(self) -> Optional[str]:
280
+ return None
281
+
282
+ def pick_from_gallery(self) -> Optional[str]:
283
+ return None
284
+
285
+
286
+ class DesktopLocation:
287
+ def get_current(self) -> Optional[Dict[str, float]]:
288
+ return None
289
+
290
+
291
+ class DesktopBiometrics:
292
+ def is_available(self) -> bool:
293
+ return False
294
+
295
+ def authenticate(self, reason: str = "Authenticate") -> bool:
296
+ del reason
297
+ return False
298
+
299
+
300
+ _DEFAULTS: Dict[str, Callable[[], Any]] = {
301
+ "Device": DesktopDevice,
302
+ "AppState": DesktopAppState,
303
+ "Storage": DesktopStorage,
304
+ "SecureStore": DesktopSecureStore,
305
+ "Clipboard": DesktopClipboard,
306
+ "Alert": DesktopAlert,
307
+ "Share": DesktopShare,
308
+ "Linking": DesktopLinking,
309
+ "Haptics": DesktopHaptics,
310
+ "Battery": DesktopBattery,
311
+ "NetInfo": DesktopNetInfo,
312
+ "Permissions": DesktopPermissions,
313
+ "Notifications": DesktopNotifications,
314
+ "Camera": DesktopCamera,
315
+ "Location": DesktopLocation,
316
+ "Biometrics": DesktopBiometrics,
317
+ }
318
+
319
+
320
+ def default_implementation(name: str) -> Optional[Callable[[], Any]]:
321
+ """Return the factory for the built-in desktop implementation of ``name``."""
322
+ return _DEFAULTS.get(name)
@@ -17,8 +17,9 @@ Example:
17
17
  import os
18
18
  from typing import Any, Optional
19
19
 
20
- from .. import diagnostics
21
- from ..utils import IS_ANDROID
20
+ from .registry import native_module
21
+
22
+ _app_dir_cache: Optional[str] = None
22
23
 
23
24
 
24
25
  class FileSystem:
@@ -39,31 +40,24 @@ class FileSystem:
39
40
  On Android the result is `Context.getFilesDir()`. On iOS it is
40
41
  the user's Documents directory. On a desktop machine without
41
42
  either runtime, a `.pythonnative_data` directory is created
42
- under the user's home folder.
43
+ under the user's home folder. The value comes from the native
44
+ ``Device`` module's ``info()`` and is cached after the first
45
+ call.
43
46
 
44
47
  Returns:
45
48
  Absolute path to the app's data directory.
46
49
  """
47
- if IS_ANDROID:
48
- try:
49
- from ..utils import get_android_context
50
-
51
- return str(get_android_context().getFilesDir().getAbsolutePath())
52
- except Exception:
53
- diagnostics.swallowed("file_system.FileSystem.app_dir")
54
- else:
50
+ global _app_dir_cache
51
+ if _app_dir_cache is None:
52
+ path: Optional[str] = None
55
53
  try:
56
- from rubicon.objc import ObjCClass
57
-
58
- NSSearchPathForDirectoriesInDomains = ObjCClass(
59
- "NSFileManager"
60
- ).defaultManager.URLsForDirectory_inDomains_
61
- docs = NSSearchPathForDirectoriesInDomains(9, 1) # NSDocumentDirectory, NSUserDomainMask
62
- if docs and docs.count > 0:
63
- return str(docs.objectAtIndex_(0).path)
54
+ info = native_module("Device").call("info")
55
+ if isinstance(info, dict):
56
+ path = info.get("app_dir")
64
57
  except Exception:
65
- diagnostics.swallowed("file_system.FileSystem.app_dir")
66
- return os.path.join(os.path.expanduser("~"), ".pythonnative_data")
58
+ path = None
59
+ _app_dir_cache = str(path) if path else os.path.join(os.path.expanduser("~"), ".pythonnative_data")
60
+ return _app_dir_cache
67
61
 
68
62
  @staticmethod
69
63
  def read_text(path: str, encoding: str = "utf-8") -> Optional[str]:
@@ -1,6 +1,6 @@
1
1
  """Haptic feedback and raw vibration.
2
2
 
3
- Two interfaces live here:
3
+ Two interfaces live here, both backed by the native ``Haptics`` module:
4
4
 
5
5
  - [`Haptics`][pythonnative.Haptics]: semantic, iOS-style feedback
6
6
  (impact / notification / selection) backed by
@@ -17,20 +17,20 @@ than errors.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Any, Optional
20
+ from typing import Any
21
21
 
22
22
  from .. import diagnostics
23
- from ..utils import IS_ANDROID, IS_IOS
23
+ from .registry import native_module
24
24
 
25
25
  ImpactStyle = str # "light" | "medium" | "heavy" | "soft" | "rigid"
26
26
  NotificationType = str # "success" | "warning" | "error"
27
27
 
28
- _IOS_IMPACT_STYLE = {"light": 0, "medium": 1, "heavy": 2, "soft": 4, "rigid": 5}
29
- _IOS_NOTIFICATION_TYPE = {"success": 0, "warning": 1, "error": 2}
30
28
 
31
- # Approximate Android fallback durations (ms) per semantic feedback.
32
- _ANDROID_IMPACT_MS = {"light": 10, "medium": 20, "heavy": 40, "soft": 15, "rigid": 30}
33
- _ANDROID_NOTIFICATION_MS = {"success": 30, "warning": 50, "error": 70}
29
+ def _call(method: str, **args: Any) -> None:
30
+ try:
31
+ native_module("Haptics").call(method, **args)
32
+ except Exception:
33
+ diagnostics.swallowed(f"haptics.{method}")
34
34
 
35
35
 
36
36
  class Haptics:
@@ -39,26 +39,17 @@ class Haptics:
39
39
  @staticmethod
40
40
  def impact(style: ImpactStyle = "medium") -> None:
41
41
  """Play a physical "impact" tap of the given ``style``."""
42
- if IS_IOS:
43
- _ios_impact(style)
44
- elif IS_ANDROID:
45
- _android_buzz(_ANDROID_IMPACT_MS.get(style, 20))
42
+ _call("impact", style=style)
46
43
 
47
44
  @staticmethod
48
45
  def notification(type_: NotificationType = "success") -> None:
49
46
  """Play a success / warning / error notification pattern."""
50
- if IS_IOS:
51
- _ios_notification(type_)
52
- elif IS_ANDROID:
53
- _android_buzz(_ANDROID_NOTIFICATION_MS.get(type_, 30))
47
+ _call("notification", type=type_)
54
48
 
55
49
  @staticmethod
56
50
  def selection() -> None:
57
51
  """Play the light "selection changed" tick."""
58
- if IS_IOS:
59
- _ios_selection()
60
- elif IS_ANDROID:
61
- _android_buzz(8)
52
+ _call("selection")
62
53
 
63
54
 
64
55
  class Vibration:
@@ -66,116 +57,15 @@ class Vibration:
66
57
 
67
58
  @staticmethod
68
59
  def vibrate(duration_ms: int = 400) -> None:
69
- """Vibrate for ``duration_ms`` milliseconds."""
70
- if IS_ANDROID:
71
- _android_buzz(int(duration_ms))
72
- elif IS_IOS:
73
- # iOS has no arbitrary-duration API; approximate with a
74
- # heavy impact for short buzzes and the legacy AudioServices
75
- # vibration for longer ones.
76
- if duration_ms >= 200:
77
- _ios_legacy_vibrate()
78
- else:
79
- _ios_impact("heavy")
60
+ """Vibrate for ``duration_ms`` milliseconds.
61
+
62
+ iOS has no arbitrary-duration API; the native module
63
+ approximates short buzzes with a heavy impact and longer ones
64
+ with the legacy system vibration sound.
65
+ """
66
+ _call("vibrate", duration_ms=int(duration_ms))
80
67
 
81
68
  @staticmethod
82
69
  def cancel() -> None:
83
70
  """Cancel an in-progress vibration (Android only)."""
84
- if IS_ANDROID:
85
- vibrator = _android_vibrator()
86
- if vibrator is not None:
87
- try:
88
- vibrator.cancel()
89
- except Exception:
90
- diagnostics.swallowed("haptics.Vibration.cancel")
91
-
92
-
93
- # ======================================================================
94
- # iOS
95
- # ======================================================================
96
-
97
-
98
- def _ios_impact(style: str) -> None:
99
- try:
100
- from rubicon.objc import ObjCClass
101
-
102
- generator = ObjCClass("UIImpactFeedbackGenerator").alloc().initWithStyle_(_IOS_IMPACT_STYLE.get(style, 1))
103
- generator.prepare()
104
- generator.impactOccurred()
105
- except Exception:
106
- diagnostics.swallowed("haptics._ios_impact")
107
-
108
-
109
- def _ios_notification(type_: str) -> None:
110
- try:
111
- from rubicon.objc import ObjCClass
112
-
113
- generator = ObjCClass("UINotificationFeedbackGenerator").alloc().init()
114
- generator.prepare()
115
- generator.notificationOccurred_(_IOS_NOTIFICATION_TYPE.get(type_, 0))
116
- except Exception:
117
- diagnostics.swallowed("haptics._ios_notification")
118
-
119
-
120
- def _ios_selection() -> None:
121
- try:
122
- from rubicon.objc import ObjCClass
123
-
124
- generator = ObjCClass("UISelectionFeedbackGenerator").alloc().init()
125
- generator.prepare()
126
- generator.selectionChanged()
127
- except Exception:
128
- diagnostics.swallowed("haptics._ios_selection")
129
-
130
-
131
- def _ios_legacy_vibrate() -> None:
132
- try:
133
- from ctypes import CDLL, util
134
-
135
- audio = CDLL(util.find_library("AudioToolbox"))
136
- audio.AudioServicesPlaySystemSound(4095) # kSystemSoundID_Vibrate
137
- except Exception:
138
- diagnostics.swallowed("haptics._ios_legacy_vibrate")
139
-
140
-
141
- # ======================================================================
142
- # Android
143
- # ======================================================================
144
-
145
-
146
- def _android_vibrator() -> Optional[Any]:
147
- try:
148
- from java import jclass
149
-
150
- from ..utils import get_android_context
151
-
152
- ctx = get_android_context()
153
- Build = jclass("android.os.Build")
154
- if Build.VERSION.SDK_INT >= 31:
155
- VibratorManager = jclass("android.os.VibratorManager")
156
- Context = jclass("android.content.Context")
157
- manager = ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE)
158
- VibratorManager # referenced for clarity
159
- return manager.getDefaultVibrator()
160
- Context = jclass("android.content.Context")
161
- return ctx.getSystemService(Context.VIBRATOR_SERVICE)
162
- except Exception:
163
- return None
164
-
165
-
166
- def _android_buzz(duration_ms: int) -> None:
167
- vibrator = _android_vibrator()
168
- if vibrator is None:
169
- return
170
- try:
171
- from java import jclass
172
-
173
- Build = jclass("android.os.Build")
174
- if Build.VERSION.SDK_INT >= 26:
175
- VibrationEffect = jclass("android.os.VibrationEffect")
176
- effect = VibrationEffect.createOneShot(int(duration_ms), VibrationEffect.DEFAULT_AMPLITUDE)
177
- vibrator.vibrate(effect)
178
- else:
179
- vibrator.vibrate(int(duration_ms))
180
- except Exception:
181
- diagnostics.swallowed("haptics._android_buzz")
71
+ _call("cancel")