pythonnative 0.36.0__py3-none-any.whl → 0.38.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. pythonnative/__init__.py +14 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +24 -86
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +96 -10
  11. pythonnative/components/_base.py +31 -0
  12. pythonnative/components/controls.py +19 -16
  13. pythonnative/components/layout.py +70 -11
  14. pythonnative/components/lists.py +6 -6
  15. pythonnative/gestures.py +40 -16
  16. pythonnative/hooks.py +41 -26
  17. pythonnative/hosts/__init__.py +13 -36
  18. pythonnative/hosts/native.py +348 -0
  19. pythonnative/images.py +6 -5
  20. pythonnative/native_modules/__init__.py +49 -29
  21. pythonnative/native_modules/app_state.py +10 -6
  22. pythonnative/native_modules/battery.py +22 -87
  23. pythonnative/native_modules/biometrics.py +15 -130
  24. pythonnative/native_modules/camera.py +24 -296
  25. pythonnative/native_modules/clipboard.py +14 -111
  26. pythonnative/native_modules/desktop.py +322 -0
  27. pythonnative/native_modules/file_system.py +90 -187
  28. pythonnative/native_modules/haptics.py +29 -135
  29. pythonnative/native_modules/linking.py +29 -141
  30. pythonnative/native_modules/location.py +32 -186
  31. pythonnative/native_modules/net_info.py +22 -135
  32. pythonnative/native_modules/notifications.py +37 -265
  33. pythonnative/native_modules/permissions.py +78 -194
  34. pythonnative/native_modules/registry.py +513 -0
  35. pythonnative/native_modules/secure_store.py +23 -164
  36. pythonnative/native_modules/share.py +9 -114
  37. pythonnative/native_views/__init__.py +67 -86
  38. pythonnative/native_views/bridge_backend.py +264 -0
  39. pythonnative/navigation/__init__.py +2 -1
  40. pythonnative/navigation/hooks.py +46 -7
  41. pythonnative/navigation/state.py +30 -9
  42. pythonnative/platform.py +19 -15
  43. pythonnative/platform_metrics.py +3 -3
  44. pythonnative/project/android.py +23 -3
  45. pythonnative/project/builder.py +99 -48
  46. pythonnative/project/config.py +96 -26
  47. pythonnative/project/deps.py +703 -0
  48. pythonnative/project/doctor.py +53 -4
  49. pythonnative/project/plugins.py +366 -0
  50. pythonnative/project/runtime_assets.py +13 -14
  51. pythonnative/py.typed +0 -0
  52. pythonnative/runtime.py +35 -203
  53. pythonnative/sdk/__init__.py +48 -33
  54. pythonnative/sdk/_components.py +57 -76
  55. pythonnative/storage.py +18 -243
  56. pythonnative/style.py +94 -44
  57. pythonnative/templates/android_template/app/build.gradle +11 -11
  58. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  59. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  60. pythonnative/templates/android_template/build.gradle +7 -4
  61. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
  62. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  103. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  105. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  106. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  107. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  108. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  109. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  110. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  111. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  112. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  113. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  114. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  115. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  116. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  117. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  118. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  119. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  120. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  121. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  122. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  123. pythonnative/templates/android_template/settings.gradle +1 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  157. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  158. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  159. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  160. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  161. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  162. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  163. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  164. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  165. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  166. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  167. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  168. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  169. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  170. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +41 -48
  171. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  172. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  173. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +82 -10
  174. pythonnative/utils.py +26 -109
  175. pythonnative/virtual_rows.py +13 -23
  176. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +10 -12
  177. pythonnative-0.38.0.dist-info/RECORD +264 -0
  178. pythonnative/hosts/android.py +0 -269
  179. pythonnative/hosts/ios.py +0 -404
  180. pythonnative/native_views/android.py +0 -3702
  181. pythonnative/native_views/ios.py +0 -5253
  182. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  183. pythonnative-0.36.0.dist-info/RECORD +0 -155
  184. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/WHEEL +0 -0
  185. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
  186. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
  187. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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)
@@ -1,9 +1,19 @@
1
- """Cross-platform app-scoped file I/O.
2
-
3
- Provides static helpers for reading, writing, and deleting files in the
4
- app's sandboxed storage area. Relative paths are resolved against
5
- [`FileSystem.app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir];
6
- absolute paths are used as-is.
1
+ """App-scoped file I/O.
2
+
3
+ [`FileSystem`][pythonnative.FileSystem] answers one question the
4
+ standard library can't, "where may this app write?", and then gets out
5
+ of the way: [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir]
6
+ comes from the native ``Device`` module, and
7
+ [`path`][pythonnative.native_modules.file_system.FileSystem.path] turns
8
+ an app-relative name into a ``pathlib.Path`` you use like any other.
9
+ The read/write helpers are thin conveniences over that path; they
10
+ raise the same ``OSError`` subclasses ``open`` and ``os`` do
11
+ (``FileNotFoundError``, ``PermissionError``, ...) rather than hiding
12
+ them behind ``None`` and ``False``.
13
+
14
+ Relative paths are resolved against ``app_dir``; absolute paths are used
15
+ as-is. Everything here is synchronous: it is local disk I/O on the
16
+ calling thread, exactly like the standard library.
7
17
 
8
18
  Example:
9
19
  ```python
@@ -11,232 +21,125 @@ Example:
11
21
 
12
22
  FileSystem.write_text("notes/today.txt", "Hello, file system!")
13
23
  print(FileSystem.read_text("notes/today.txt"))
24
+
25
+ # Or work with the Path directly:
26
+ notes = FileSystem.path("notes")
27
+ for entry in sorted(notes.iterdir()):
28
+ print(entry.name)
14
29
  ```
15
30
  """
16
31
 
32
+ from __future__ import annotations
33
+
17
34
  import os
18
- from typing import Any, Optional
35
+ from pathlib import Path
36
+ from typing import Any, List, Optional, Union
37
+
38
+ from .registry import native_module
19
39
 
20
- from .. import diagnostics
21
- from ..utils import IS_ANDROID
40
+ PathLike = Union[str, "os.PathLike[str]"]
41
+
42
+ _app_dir_cache: Optional[str] = None
22
43
 
23
44
 
24
45
  class FileSystem:
25
46
  """App-scoped file I/O.
26
47
 
27
- Every instance method operates on either an absolute path or a path
28
- relative to
29
- [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir].
30
- Errors are swallowed and reported as falsy return values (`None`
31
- for readers, `False` for writers) so callers can treat the API as
32
- best-effort.
48
+ Every helper accepts an absolute path or a path relative to
49
+ [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir],
50
+ and raises ``OSError`` (or a subclass) when the operation fails,
51
+ like the standard library it wraps.
33
52
  """
34
53
 
35
54
  @staticmethod
36
55
  def app_dir() -> str:
37
56
  """Return the app's writable data directory.
38
57
 
39
- On Android the result is `Context.getFilesDir()`. On iOS it is
40
- the user's Documents directory. On a desktop machine without
41
- either runtime, a `.pythonnative_data` directory is created
42
- under the user's home folder.
58
+ On Android the result is ``Context.getFilesDir()``. On iOS it is
59
+ the app's Documents directory. On a desktop machine without
60
+ either runtime, a ``.pythonnative_data`` directory under the
61
+ user's home folder is used. The value comes from the native
62
+ ``Device`` module's ``info()`` and is cached after the first
63
+ call.
43
64
 
44
65
  Returns:
45
66
  Absolute path to the app's data directory.
46
67
  """
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:
55
- 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)
64
- except Exception:
65
- diagnostics.swallowed("file_system.FileSystem.app_dir")
66
- return os.path.join(os.path.expanduser("~"), ".pythonnative_data")
68
+ global _app_dir_cache
69
+ if _app_dir_cache is None:
70
+ info = native_module("Device").call("info")
71
+ path = info.get("app_dir") if isinstance(info, dict) else None
72
+ _app_dir_cache = str(path) if path else os.path.join(os.path.expanduser("~"), ".pythonnative_data")
73
+ return _app_dir_cache
67
74
 
68
75
  @staticmethod
69
- def read_text(path: str, encoding: str = "utf-8") -> Optional[str]:
70
- """Read a text file.
71
-
72
- Args:
73
- path: Absolute path or path relative to
74
- [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir].
75
- encoding: Text encoding (default `"utf-8"`).
76
+ def path(path: PathLike = "") -> Path:
77
+ """Resolve ``path`` against ``app_dir`` and return it as a ``pathlib.Path``.
76
78
 
77
- Returns:
78
- File contents as a `str`, or `None` if the file cannot be
79
- read.
79
+ Absolute paths are returned unchanged. With no argument, returns
80
+ ``app_dir`` itself.
80
81
  """
81
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
82
- try:
83
- with open(full, encoding=encoding) as f:
84
- return f.read()
85
- except OSError:
86
- return None
82
+ candidate = Path(path)
83
+ return candidate if candidate.is_absolute() else Path(FileSystem.app_dir()) / candidate
87
84
 
88
85
  @staticmethod
89
- def write_text(path: str, content: str, encoding: str = "utf-8") -> bool:
90
- """Write a text file, creating parent directories as needed.
91
-
92
- Args:
93
- path: Absolute or `app_dir`-relative path.
94
- content: String to write.
95
- encoding: Text encoding (default `"utf-8"`).
96
-
97
- Returns:
98
- `True` on success, `False` on `OSError`.
99
- """
100
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
101
- try:
102
- os.makedirs(os.path.dirname(full), exist_ok=True)
103
- with open(full, "w", encoding=encoding) as f:
104
- f.write(content)
105
- return True
106
- except OSError:
107
- return False
86
+ def read_text(path: PathLike, encoding: str = "utf-8") -> str:
87
+ """Read a text file; raises ``FileNotFoundError`` and friends like ``open`` does."""
88
+ return FileSystem.path(path).read_text(encoding=encoding)
108
89
 
109
90
  @staticmethod
110
- def exists(path: str) -> bool:
111
- """Return whether a file or directory exists.
112
-
113
- Args:
114
- path: Absolute or `app_dir`-relative path.
115
-
116
- Returns:
117
- `True` if the path exists.
118
- """
119
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
120
- return os.path.exists(full)
91
+ def write_text(path: PathLike, content: str, encoding: str = "utf-8") -> None:
92
+ """Write a text file, creating parent directories as needed."""
93
+ target = FileSystem.path(path)
94
+ target.parent.mkdir(parents=True, exist_ok=True)
95
+ target.write_text(content, encoding=encoding)
121
96
 
122
97
  @staticmethod
123
- def delete(path: str) -> bool:
124
- """Delete a single file.
125
-
126
- Args:
127
- path: Absolute or `app_dir`-relative path.
128
-
129
- Returns:
130
- `True` on success, `False` on `OSError`.
131
- """
132
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
133
- try:
134
- os.remove(full)
135
- return True
136
- except OSError:
137
- return False
98
+ def read_bytes(path: PathLike) -> bytes:
99
+ """Read a binary file."""
100
+ return FileSystem.path(path).read_bytes()
138
101
 
139
102
  @staticmethod
140
- def list_dir(path: str = "") -> list:
141
- """List the entries in a directory.
142
-
143
- Args:
144
- path: Absolute or `app_dir`-relative path. Defaults to the
145
- app data directory itself.
146
-
147
- Returns:
148
- A list of entry names, or an empty list on error.
149
- """
150
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
151
- try:
152
- return os.listdir(full)
153
- except OSError:
154
- return []
103
+ def write_bytes(path: PathLike, data: bytes) -> None:
104
+ """Write a binary file, creating parent directories as needed."""
105
+ target = FileSystem.path(path)
106
+ target.parent.mkdir(parents=True, exist_ok=True)
107
+ target.write_bytes(data)
155
108
 
156
109
  @staticmethod
157
- def read_bytes(path: str) -> Optional[bytes]:
158
- """Read a binary file.
159
-
160
- Args:
161
- path: Absolute or `app_dir`-relative path.
162
-
163
- Returns:
164
- File contents as `bytes`, or `None` if the file cannot be
165
- read.
166
- """
167
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
168
- try:
169
- with open(full, "rb") as f:
170
- return f.read()
171
- except OSError:
172
- return None
110
+ def exists(path: PathLike) -> bool:
111
+ """Return whether a file or directory exists."""
112
+ return FileSystem.path(path).exists()
173
113
 
174
114
  @staticmethod
175
- def write_bytes(path: str, data: bytes) -> bool:
176
- """Write a binary file, creating parent directories as needed.
115
+ def delete(path: PathLike, *, missing_ok: bool = False) -> None:
116
+ """Delete a single file.
177
117
 
178
118
  Args:
179
- path: Absolute or `app_dir`-relative path.
180
- data: Bytes to write.
181
-
182
- Returns:
183
- `True` on success, `False` on `OSError`.
119
+ path: Absolute or ``app_dir``-relative path.
120
+ missing_ok: Ignore a missing file instead of raising
121
+ ``FileNotFoundError`` (same as ``Path.unlink``).
184
122
  """
185
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
186
- try:
187
- os.makedirs(os.path.dirname(full), exist_ok=True)
188
- with open(full, "wb") as f:
189
- f.write(data)
190
- return True
191
- except OSError:
192
- return False
123
+ FileSystem.path(path).unlink(missing_ok=missing_ok)
193
124
 
194
125
  @staticmethod
195
- def get_size(path: str) -> Optional[int]:
196
- """Return file size in bytes.
197
-
198
- Args:
199
- path: Absolute or `app_dir`-relative path.
200
-
201
- Returns:
202
- File size in bytes, or `None` if the file is missing or
203
- unreadable.
204
- """
205
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
206
- try:
207
- return os.path.getsize(full)
208
- except OSError:
209
- return None
126
+ def list_dir(path: PathLike = "") -> List[str]:
127
+ """Return the entry names in a directory (``app_dir`` by default), sorted."""
128
+ return sorted(entry.name for entry in FileSystem.path(path).iterdir())
210
129
 
211
130
  @staticmethod
212
- def ensure_dir(path: str) -> bool:
213
- """Create a directory (and any missing parents) idempotently.
214
-
215
- Args:
216
- path: Absolute or `app_dir`-relative path.
131
+ def get_size(path: PathLike) -> int:
132
+ """Return a file's size in bytes."""
133
+ return FileSystem.path(path).stat().st_size
217
134
 
218
- Returns:
219
- `True` on success or if the directory already exists.
220
- """
221
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
222
- try:
223
- os.makedirs(full, exist_ok=True)
224
- return True
225
- except OSError:
226
- return False
135
+ @staticmethod
136
+ def ensure_dir(path: PathLike) -> Path:
137
+ """Create a directory (and any missing parents) if needed; returns its ``Path``."""
138
+ target = FileSystem.path(path)
139
+ target.mkdir(parents=True, exist_ok=True)
140
+ return target
227
141
 
228
142
  @staticmethod
229
143
  def join(*parts: Any) -> str:
230
- """Join path components using the OS separator.
231
-
232
- Equivalent to `os.path.join(*map(str, parts))`. Provided as a
233
- convenience so callers do not need to import `os.path`
234
- directly.
235
-
236
- Args:
237
- *parts: Path components (each coerced to `str`).
238
-
239
- Returns:
240
- The joined path string.
241
- """
144
+ """Join path components with the OS separator (``os.path.join`` over ``str(part)``)."""
242
145
  return os.path.join(*[str(p) for p in parts])