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.
- pythonnative/__init__.py +14 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +24 -86
- 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/cli/pn.py +96 -10
- pythonnative/components/_base.py +31 -0
- pythonnative/components/controls.py +19 -16
- pythonnative/components/layout.py +70 -11
- pythonnative/components/lists.py +6 -6
- pythonnative/gestures.py +40 -16
- 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 +49 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +22 -87
- pythonnative/native_modules/biometrics.py +15 -130
- pythonnative/native_modules/camera.py +24 -296
- pythonnative/native_modules/clipboard.py +14 -111
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +90 -187
- pythonnative/native_modules/haptics.py +29 -135
- pythonnative/native_modules/linking.py +29 -141
- pythonnative/native_modules/location.py +32 -186
- pythonnative/native_modules/net_info.py +22 -135
- pythonnative/native_modules/notifications.py +37 -265
- pythonnative/native_modules/permissions.py +78 -194
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +23 -164
- pythonnative/native_modules/share.py +9 -114
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/navigation/__init__.py +2 -1
- pythonnative/navigation/hooks.py +46 -7
- pythonnative/navigation/state.py +30 -9
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +23 -3
- pythonnative/project/builder.py +99 -48
- pythonnative/project/config.py +96 -26
- pythonnative/project/deps.py +703 -0
- pythonnative/project/doctor.py +53 -4
- pythonnative/project/plugins.py +366 -0
- pythonnative/project/runtime_assets.py +13 -14
- 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/style.py +94 -44
- pythonnative/templates/android_template/app/build.gradle +11 -11
- 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/build.gradle +7 -4
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
- 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 +99 -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 +266 -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 +41 -48
- 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 +82 -10
- pythonnative/utils.py +26 -109
- pythonnative/virtual_rows.py +13 -23
- {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +10 -12
- pythonnative-0.38.0.dist-info/RECORD +264 -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.38.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[`
|
|
6
|
-
|
|
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
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, List, Optional, Union
|
|
37
|
+
|
|
38
|
+
from .registry import native_module
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
40
|
-
the
|
|
41
|
-
either runtime, a
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
70
|
-
"""
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
read.
|
|
79
|
+
Absolute paths are returned unchanged. With no argument, returns
|
|
80
|
+
``app_dir`` itself.
|
|
80
81
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
90
|
-
"""
|
|
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
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
124
|
-
"""
|
|
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
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
158
|
-
"""
|
|
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
|
|
176
|
-
"""
|
|
115
|
+
def delete(path: PathLike, *, missing_ok: bool = False) -> None:
|
|
116
|
+
"""Delete a single file.
|
|
177
117
|
|
|
178
118
|
Args:
|
|
179
|
-
path: Absolute or
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
196
|
-
"""Return
|
|
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
|
|
213
|
-
"""
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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])
|