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.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.36.0.dist-info/RECORD +0 -155
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
21
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from .. import diagnostics
|
|
23
|
-
from
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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")
|