pythonnative 0.35.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/cli/pn.py +24 -6
- 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/mutations.py +21 -0
- 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.35.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.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Native plugin staging for ``pn build``.
|
|
2
|
+
|
|
3
|
+
A PyPI package can ship Swift and Kotlin next to its Python facade. It
|
|
4
|
+
declares an entry point in the ``pythonnative.plugins`` group whose
|
|
5
|
+
value is an importable package directory containing:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
my_blur/native/
|
|
9
|
+
pn_plugin.json
|
|
10
|
+
ios/BlurViewManager.swift
|
|
11
|
+
android/com/example/myblur/BlurViewManager.kt
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
``pn_plugin.json`` names the registration entry on each platform:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{"ios": {"entry": "MyBlurPlugin"}, "android": {"entry": "com.example.myblur.MyBlurPlugin"}}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
At build time this module copies each plugin's sources into the staged
|
|
21
|
+
native project (``PythonNativeKit/Sources/PythonNativeKit/Plugins/<name>``
|
|
22
|
+
on iOS, ``pythonnative/src/main/java`` on Android) and regenerates the
|
|
23
|
+
registration file that calls every entry's ``register``. SwiftPM and
|
|
24
|
+
Gradle compile whatever lands under those roots, so no project-file
|
|
25
|
+
surgery is needed.
|
|
26
|
+
|
|
27
|
+
Projects can also bundle plugins that aren't installed as distributions
|
|
28
|
+
by listing directories under ``[plugins].paths`` in ``pythonnative.toml``;
|
|
29
|
+
each path must contain the same ``pn_plugin.json`` layout.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
import shutil
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from importlib import import_module
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Callable, Dict, Iterable, List, Optional, Sequence
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ENTRY_POINT_GROUP",
|
|
44
|
+
"MANIFEST_NAME",
|
|
45
|
+
"NativePlugin",
|
|
46
|
+
"PluginError",
|
|
47
|
+
"discover_plugins",
|
|
48
|
+
"load_plugin",
|
|
49
|
+
"stage_android_plugins",
|
|
50
|
+
"stage_ios_plugins",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
ENTRY_POINT_GROUP = "pythonnative.plugins"
|
|
54
|
+
"""Entry-point group whose values name plugin directories (as packages)."""
|
|
55
|
+
|
|
56
|
+
MANIFEST_NAME = "pn_plugin.json"
|
|
57
|
+
"""File name of the per-plugin manifest."""
|
|
58
|
+
|
|
59
|
+
Logger = Callable[[str], None]
|
|
60
|
+
|
|
61
|
+
_IOS_PLUGIN_DIR = ("PythonNativeKit", "Sources", "PythonNativeKit", "Plugins")
|
|
62
|
+
_IOS_REGISTRATION = (*_IOS_PLUGIN_DIR, "PNPluginRegistration.swift")
|
|
63
|
+
_ANDROID_SOURCE_ROOT = ("pythonnative", "src", "main", "java")
|
|
64
|
+
_ANDROID_REGISTRATION = (*_ANDROID_SOURCE_ROOT, "com", "pythonnative", "runtime", "plugins", "GeneratedPlugins.kt")
|
|
65
|
+
|
|
66
|
+
_SWIFT_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
67
|
+
_KOTLIN_FQN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PluginError(RuntimeError):
|
|
71
|
+
"""A plugin manifest or source layout is invalid."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class NativePlugin:
|
|
76
|
+
"""A discovered native plugin.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
name: Stable identifier (the entry-point name or the directory
|
|
80
|
+
name for path plugins). Used for the iOS subfolder.
|
|
81
|
+
root: Directory containing ``pn_plugin.json``.
|
|
82
|
+
ios_entry: Swift type conforming to ``PNPlugin``, or ``None``
|
|
83
|
+
when the plugin has no iOS side.
|
|
84
|
+
android_entry: Fully qualified Kotlin object implementing
|
|
85
|
+
``PNPlugin``, or ``None`` when the plugin has no Android side.
|
|
86
|
+
ios_sources: Swift files under ``ios/``.
|
|
87
|
+
android_sources: Kotlin files under ``android/`` (relative
|
|
88
|
+
paths are preserved so packages land in the right folders).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
root: Path
|
|
93
|
+
ios_entry: Optional[str] = None
|
|
94
|
+
android_entry: Optional[str] = None
|
|
95
|
+
ios_sources: Sequence[Path] = field(default_factory=tuple)
|
|
96
|
+
android_sources: Sequence[Path] = field(default_factory=tuple)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def has_ios(self) -> bool:
|
|
100
|
+
"""Whether this plugin contributes Swift code."""
|
|
101
|
+
return self.ios_entry is not None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def has_android(self) -> bool:
|
|
105
|
+
"""Whether this plugin contributes Kotlin code."""
|
|
106
|
+
return self.android_entry is not None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ======================================================================
|
|
110
|
+
# Discovery
|
|
111
|
+
# ======================================================================
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_plugin(root: Path, *, name: Optional[str] = None) -> NativePlugin:
|
|
115
|
+
"""Read ``pn_plugin.json`` under ``root`` and collect its sources.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
root: Directory containing the manifest.
|
|
119
|
+
name: Identifier override (defaults to the directory name).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The parsed plugin.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
PluginError: If the manifest is missing or malformed, or an
|
|
126
|
+
entry is declared without any matching sources.
|
|
127
|
+
"""
|
|
128
|
+
root = Path(root).resolve()
|
|
129
|
+
manifest_path = root / MANIFEST_NAME
|
|
130
|
+
if not manifest_path.is_file():
|
|
131
|
+
raise PluginError(f"{root} has no {MANIFEST_NAME}.")
|
|
132
|
+
try:
|
|
133
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
134
|
+
except (OSError, ValueError) as exc:
|
|
135
|
+
raise PluginError(f"Could not parse {manifest_path}: {exc}") from exc
|
|
136
|
+
if not isinstance(manifest, dict):
|
|
137
|
+
raise PluginError(f"{manifest_path} must contain a JSON object.")
|
|
138
|
+
|
|
139
|
+
plugin_name = name or manifest.get("name") or root.name
|
|
140
|
+
if not _SWIFT_IDENT.match(str(plugin_name).replace("-", "_")):
|
|
141
|
+
raise PluginError(f"Plugin name {plugin_name!r} must be an identifier.")
|
|
142
|
+
plugin_name = str(plugin_name).replace("-", "_")
|
|
143
|
+
|
|
144
|
+
ios_entry = _entry(manifest, "ios", manifest_path)
|
|
145
|
+
android_entry = _entry(manifest, "android", manifest_path)
|
|
146
|
+
if ios_entry is None and android_entry is None:
|
|
147
|
+
raise PluginError(f"{manifest_path} declares neither an 'ios' nor an 'android' entry.")
|
|
148
|
+
|
|
149
|
+
ios_sources: List[Path] = []
|
|
150
|
+
android_sources: List[Path] = []
|
|
151
|
+
if ios_entry is not None:
|
|
152
|
+
if not _SWIFT_IDENT.match(ios_entry):
|
|
153
|
+
raise PluginError(f"{manifest_path}: ios.entry {ios_entry!r} is not a Swift type name.")
|
|
154
|
+
ios_sources = sorted(p.relative_to(root) for p in (root / "ios").rglob("*.swift") if p.is_file())
|
|
155
|
+
if not ios_sources:
|
|
156
|
+
raise PluginError(f"{root} declares an iOS entry but ios/ contains no .swift files.")
|
|
157
|
+
if android_entry is not None:
|
|
158
|
+
if not _KOTLIN_FQN.match(android_entry):
|
|
159
|
+
raise PluginError(f"{manifest_path}: android.entry {android_entry!r} must be a fully qualified name.")
|
|
160
|
+
android_sources = sorted(
|
|
161
|
+
p.relative_to(root) for p in (root / "android").rglob("*") if p.is_file() and p.suffix in (".kt", ".java")
|
|
162
|
+
)
|
|
163
|
+
if not android_sources:
|
|
164
|
+
raise PluginError(f"{root} declares an Android entry but android/ contains no .kt or .java files.")
|
|
165
|
+
|
|
166
|
+
return NativePlugin(
|
|
167
|
+
name=plugin_name,
|
|
168
|
+
root=root,
|
|
169
|
+
ios_entry=ios_entry,
|
|
170
|
+
android_entry=android_entry,
|
|
171
|
+
ios_sources=tuple(ios_sources),
|
|
172
|
+
android_sources=tuple(android_sources),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _entry(manifest: Dict[str, object], platform: str, manifest_path: Path) -> Optional[str]:
|
|
177
|
+
section = manifest.get(platform)
|
|
178
|
+
if section is None:
|
|
179
|
+
return None
|
|
180
|
+
if not isinstance(section, dict) or not isinstance(section.get("entry"), str):
|
|
181
|
+
raise PluginError(f"{manifest_path}: '{platform}' must be an object with a string 'entry'.")
|
|
182
|
+
return str(section["entry"])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def discover_plugins(
|
|
186
|
+
*,
|
|
187
|
+
extra_paths: Iterable[Path] = (),
|
|
188
|
+
log: Optional[Logger] = None,
|
|
189
|
+
) -> List[NativePlugin]:
|
|
190
|
+
"""Find every plugin visible to this interpreter.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
extra_paths: Plugin directories to include in addition to the
|
|
194
|
+
``pythonnative.plugins`` entry points (from
|
|
195
|
+
``[plugins].paths`` in the project config).
|
|
196
|
+
log: Optional progress logger; broken entry points are reported
|
|
197
|
+
here and skipped rather than failing the build.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Plugins in a stable order (entry points sorted by name, then
|
|
201
|
+
explicit paths in the order given).
|
|
202
|
+
"""
|
|
203
|
+
emit: Logger = log or (lambda _message: None)
|
|
204
|
+
plugins: List[NativePlugin] = []
|
|
205
|
+
seen: set = set()
|
|
206
|
+
|
|
207
|
+
for ep_name, target in sorted(_entry_points().items()):
|
|
208
|
+
try:
|
|
209
|
+
root = _resolve_entry_point(target)
|
|
210
|
+
plugin = load_plugin(root, name=ep_name)
|
|
211
|
+
except (PluginError, ImportError, AttributeError) as exc:
|
|
212
|
+
emit(f"Skipping native plugin {ep_name!r}: {exc}")
|
|
213
|
+
continue
|
|
214
|
+
if plugin.name in seen:
|
|
215
|
+
emit(f"Skipping duplicate native plugin {plugin.name!r}.")
|
|
216
|
+
continue
|
|
217
|
+
seen.add(plugin.name)
|
|
218
|
+
plugins.append(plugin)
|
|
219
|
+
|
|
220
|
+
for path in extra_paths:
|
|
221
|
+
plugin = load_plugin(Path(path))
|
|
222
|
+
if plugin.name in seen:
|
|
223
|
+
raise PluginError(f"Native plugin {plugin.name!r} is declared twice.")
|
|
224
|
+
seen.add(plugin.name)
|
|
225
|
+
plugins.append(plugin)
|
|
226
|
+
|
|
227
|
+
return plugins
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _entry_points() -> Dict[str, str]:
|
|
231
|
+
try:
|
|
232
|
+
from importlib.metadata import entry_points
|
|
233
|
+
except ImportError: # pragma: no cover - Python < 3.8
|
|
234
|
+
return {}
|
|
235
|
+
try:
|
|
236
|
+
eps = entry_points()
|
|
237
|
+
except Exception:
|
|
238
|
+
return {}
|
|
239
|
+
selected = []
|
|
240
|
+
if hasattr(eps, "select"):
|
|
241
|
+
selected = list(eps.select(group=ENTRY_POINT_GROUP))
|
|
242
|
+
else: # pragma: no cover - importlib.metadata < 3.10
|
|
243
|
+
getter = getattr(eps, "get", None)
|
|
244
|
+
if callable(getter):
|
|
245
|
+
selected = list(getter(ENTRY_POINT_GROUP, []))
|
|
246
|
+
return {ep.name: ep.value for ep in selected}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _resolve_entry_point(target: str) -> Path:
|
|
250
|
+
"""Turn ``"pkg.subpkg"`` (or ``"pkg.subpkg:attr"``) into a directory."""
|
|
251
|
+
module_name, _, attr = target.partition(":")
|
|
252
|
+
module = import_module(module_name.strip())
|
|
253
|
+
if attr:
|
|
254
|
+
value = getattr(module, attr.strip())
|
|
255
|
+
return Path(str(value))
|
|
256
|
+
location = getattr(module, "__file__", None)
|
|
257
|
+
if location is None:
|
|
258
|
+
raise PluginError(f"{module_name!r} is a namespace package; point the entry point at a regular package.")
|
|
259
|
+
return Path(location).resolve().parent
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ======================================================================
|
|
263
|
+
# Staging
|
|
264
|
+
# ======================================================================
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def stage_ios_plugins(project_dir: Path, plugins: Sequence[NativePlugin], *, log: Optional[Logger] = None) -> None:
|
|
268
|
+
"""Copy Swift sources into ``PythonNativeKit`` and write the registration file.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
project_dir: The staged ``ios_template`` directory.
|
|
272
|
+
plugins: Plugins to bundle; ones without an iOS side are skipped.
|
|
273
|
+
log: Optional progress logger.
|
|
274
|
+
"""
|
|
275
|
+
emit: Logger = log or (lambda _message: None)
|
|
276
|
+
plugins_dir = Path(project_dir).joinpath(*_IOS_PLUGIN_DIR)
|
|
277
|
+
registration = Path(project_dir).joinpath(*_IOS_REGISTRATION)
|
|
278
|
+
plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
# Wipe previous plugin folders so removed plugins don't linger in an
|
|
281
|
+
# incremental build directory; the registration file is regenerated.
|
|
282
|
+
for child in plugins_dir.iterdir():
|
|
283
|
+
if child.is_dir():
|
|
284
|
+
shutil.rmtree(child)
|
|
285
|
+
|
|
286
|
+
bundled = [p for p in plugins if p.has_ios]
|
|
287
|
+
for plugin in bundled:
|
|
288
|
+
target = plugins_dir / plugin.name
|
|
289
|
+
for rel in plugin.ios_sources:
|
|
290
|
+
dest = target / Path(*rel.parts[1:]) # strip the leading "ios/"
|
|
291
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
shutil.copy2(plugin.root / rel, dest)
|
|
293
|
+
registration.write_text(_render_swift_registration(bundled), encoding="utf-8")
|
|
294
|
+
if bundled:
|
|
295
|
+
emit(
|
|
296
|
+
f"Bundled {len(bundled)} native plugin(s) into PythonNativeKit: " + ", ".join(p.name for p in bundled) + "."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def stage_android_plugins(project_dir: Path, plugins: Sequence[NativePlugin], *, log: Optional[Logger] = None) -> None:
|
|
301
|
+
"""Copy Kotlin sources into the ``pythonnative`` module and write the registration file.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
project_dir: The staged ``android_template`` directory.
|
|
305
|
+
plugins: Plugins to bundle; ones without an Android side are skipped.
|
|
306
|
+
log: Optional progress logger.
|
|
307
|
+
"""
|
|
308
|
+
emit: Logger = log or (lambda _message: None)
|
|
309
|
+
source_root = Path(project_dir).joinpath(*_ANDROID_SOURCE_ROOT)
|
|
310
|
+
registration = Path(project_dir).joinpath(*_ANDROID_REGISTRATION)
|
|
311
|
+
source_root.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
|
|
313
|
+
bundled = [p for p in plugins if p.has_android]
|
|
314
|
+
for plugin in bundled:
|
|
315
|
+
for rel in plugin.android_sources:
|
|
316
|
+
dest = source_root / Path(*rel.parts[1:]) # strip the leading "android/"
|
|
317
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
+
shutil.copy2(plugin.root / rel, dest)
|
|
319
|
+
registration.parent.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
registration.write_text(_render_kotlin_registration(bundled), encoding="utf-8")
|
|
321
|
+
if bundled:
|
|
322
|
+
emit(
|
|
323
|
+
f"Bundled {len(bundled)} native plugin(s) into the pythonnative module: "
|
|
324
|
+
+ ", ".join(p.name for p in bundled)
|
|
325
|
+
+ "."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _render_swift_registration(plugins: Sequence[NativePlugin]) -> str:
|
|
330
|
+
calls = "".join(f" {p.ios_entry}.register(into: registry)\n" for p in plugins)
|
|
331
|
+
body = f" {{\n{calls} }}" if calls else " {}"
|
|
332
|
+
return (
|
|
333
|
+
"// Generated by `pn build`. Do not edit: the PythonNative build tool\n"
|
|
334
|
+
"// overwrites this file with one `register(into:)` call per bundled plugin.\n"
|
|
335
|
+
"\n"
|
|
336
|
+
"/// Registration hook for plugins copied into `Sources/Plugins` by `pn build`.\n"
|
|
337
|
+
"public enum PNGeneratedPlugins {\n"
|
|
338
|
+
" /// Register every generated plugin into `registry`.\n"
|
|
339
|
+
f" public static func registerAll(into registry: PNRegistry){body}\n"
|
|
340
|
+
"}\n"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _render_kotlin_registration(plugins: Sequence[NativePlugin]) -> str:
|
|
345
|
+
calls = "".join(f" {p.android_entry}.register(registry)\n" for p in plugins)
|
|
346
|
+
if calls:
|
|
347
|
+
method = f" fun registerAll(registry: PNRegistry) {{\n{calls} }}\n"
|
|
348
|
+
else:
|
|
349
|
+
method = ' @Suppress("UNUSED_PARAMETER")\n fun registerAll(registry: PNRegistry) {}\n'
|
|
350
|
+
return (
|
|
351
|
+
"package com.pythonnative.runtime.plugins\n"
|
|
352
|
+
"\n"
|
|
353
|
+
"import com.pythonnative.runtime.bridge.PNRegistry\n"
|
|
354
|
+
"\n"
|
|
355
|
+
"/**\n"
|
|
356
|
+
" * Plugin registrations generated by `pn build`.\n"
|
|
357
|
+
" *\n"
|
|
358
|
+
" * The build tool overwrites this file with one `register` call per\n"
|
|
359
|
+
" * plugin entry declared in the project's `pythonnative.plugins` entry\n"
|
|
360
|
+
" * points. Nothing else may depend on its content beyond [registerAll].\n"
|
|
361
|
+
" */\n"
|
|
362
|
+
"object GeneratedPlugins {\n"
|
|
363
|
+
" /** Register every generated plugin with `registry`. */\n"
|
|
364
|
+
f"{method}"
|
|
365
|
+
"}\n"
|
|
366
|
+
)
|
pythonnative/py.typed
ADDED
|
File without changes
|
pythonnative/runtime.py
CHANGED
|
@@ -47,12 +47,10 @@ Example:
|
|
|
47
47
|
from __future__ import annotations
|
|
48
48
|
|
|
49
49
|
import asyncio
|
|
50
|
-
import ctypes
|
|
51
|
-
import ctypes.util
|
|
52
50
|
import inspect
|
|
53
51
|
import threading
|
|
54
52
|
import time as _time
|
|
55
|
-
from typing import Any, Awaitable, Callable, Coroutine,
|
|
53
|
+
from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar, Union
|
|
56
54
|
|
|
57
55
|
T = TypeVar("T")
|
|
58
56
|
|
|
@@ -463,28 +461,41 @@ def _has_pump_dispatcher() -> bool:
|
|
|
463
461
|
|
|
464
462
|
|
|
465
463
|
def _dispatch_to_main_queue(fn: Callable[[], None]) -> None:
|
|
466
|
-
"""Enqueue ``fn`` on the platform main queue (never runs inline).
|
|
464
|
+
"""Enqueue ``fn`` on the platform main queue (never runs inline).
|
|
465
|
+
|
|
466
|
+
On device this is the bridge's
|
|
467
|
+
[`post_to_main`][pythonnative.bridge.post_to_main]: native schedules
|
|
468
|
+
a ``pump`` callback on the next main-queue turn and Python drains
|
|
469
|
+
its queue then. Off device the ``pn preview`` dispatcher takes it;
|
|
470
|
+
headless tests have no dispatcher and drive the loop with
|
|
471
|
+
[`drain`][pythonnative.runtime.drain] instead.
|
|
472
|
+
"""
|
|
467
473
|
from .platform import Platform
|
|
468
474
|
|
|
469
|
-
if Platform.is_ios:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
475
|
+
if Platform.is_ios or Platform.is_android:
|
|
476
|
+
from .bridge import post_to_main
|
|
477
|
+
|
|
478
|
+
post_to_main(fn)
|
|
473
479
|
elif _desktop_main_dispatch is not None:
|
|
474
480
|
_desktop_main_dispatch(fn)
|
|
475
|
-
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _is_main_thread() -> bool:
|
|
484
|
+
"""Whether the calling thread is the platform main thread.
|
|
485
|
+
|
|
486
|
+
Python is initialized on the platform main thread by both app
|
|
487
|
+
templates, so ``threading.main_thread()`` is the UIKit / Android
|
|
488
|
+
main thread on device.
|
|
489
|
+
"""
|
|
490
|
+
return threading.current_thread() is threading.main_thread()
|
|
476
491
|
|
|
477
492
|
|
|
478
493
|
def call_on_main_thread(fn: Callable[[], None]) -> None:
|
|
479
494
|
"""Run ``fn()`` on the platform UI thread.
|
|
480
495
|
|
|
481
|
-
- **iOS**:
|
|
482
|
-
``
|
|
483
|
-
|
|
484
|
-
already on the main thread.
|
|
485
|
-
- **Android**: posts a ``Runnable`` to
|
|
486
|
-
``Handler(Looper.getMainLooper())``, or runs inline on the main
|
|
487
|
-
looper thread.
|
|
496
|
+
- **iOS / Android**: runs inline when already on the main thread,
|
|
497
|
+
otherwise queues ``fn`` through the bridge (one native crossing
|
|
498
|
+
per batch of queued callables).
|
|
488
499
|
- **Desktop**: enqueues ``fn`` for the ``pn preview`` poll loop
|
|
489
500
|
(or runs inline if no preview is live).
|
|
490
501
|
- **Tests**: runs ``fn()`` inline.
|
|
@@ -498,199 +509,20 @@ def call_on_main_thread(fn: Callable[[], None]) -> None:
|
|
|
498
509
|
"""
|
|
499
510
|
from .platform import Platform
|
|
500
511
|
|
|
501
|
-
if Platform.is_ios:
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
512
|
+
if Platform.is_ios or Platform.is_android:
|
|
513
|
+
if _is_main_thread():
|
|
514
|
+
try:
|
|
515
|
+
fn()
|
|
516
|
+
except Exception as exc:
|
|
517
|
+
print(f"[pn.runtime] main-inline callback raised: {exc!r}")
|
|
518
|
+
return
|
|
519
|
+
_dispatch_to_main_queue(fn)
|
|
505
520
|
elif Platform.is_desktop and _desktop_main_dispatch is not None:
|
|
506
521
|
_desktop_main_dispatch(fn)
|
|
507
522
|
else:
|
|
508
523
|
fn()
|
|
509
524
|
|
|
510
525
|
|
|
511
|
-
# ----------------------------------------------------------------------
|
|
512
|
-
# iOS main-queue dispatch (PyDLL-based, no GIL release)
|
|
513
|
-
# ----------------------------------------------------------------------
|
|
514
|
-
#
|
|
515
|
-
# rubicon-objc loads libobjc via ctypes.CDLL, which RELEASES THE GIL
|
|
516
|
-
# around every ObjC method call. On the iOS simulator (and to a lesser
|
|
517
|
-
# extent on-device), a thread is then parked by the kernel until the
|
|
518
|
-
# next wakeup-coalescing tick (~500ms), making cross-thread ObjC calls
|
|
519
|
-
# absurdly slow. To avoid this, we call libdispatch's
|
|
520
|
-
# ``dispatch_async_f`` directly via ``ctypes.PyDLL`` (which keeps the
|
|
521
|
-
# GIL held), and pass a ``CFUNCTYPE`` trampoline as the work function.
|
|
522
|
-
# The trampoline runs on the main thread, looks up the pending Python
|
|
523
|
-
# callable by an integer key, and invokes it.
|
|
524
|
-
|
|
525
|
-
_dispatch_lib: Optional[Any] = None
|
|
526
|
-
_dispatch_async_f_c: Optional[Any] = None
|
|
527
|
-
_dispatch_main_q_ptr: int = 0
|
|
528
|
-
_DISPATCH_FN_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
|
|
529
|
-
_main_pending: Dict[int, Callable[[], None]] = {}
|
|
530
|
-
_main_pending_lock = threading.Lock()
|
|
531
|
-
_main_next_id: int = 0
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def _main_trampoline_py(ctx: int) -> None:
|
|
535
|
-
"""Run on the main thread; pop the queued callable and call it."""
|
|
536
|
-
with _main_pending_lock:
|
|
537
|
-
fn = _main_pending.pop(ctx, None)
|
|
538
|
-
if fn is None:
|
|
539
|
-
return
|
|
540
|
-
try:
|
|
541
|
-
fn()
|
|
542
|
-
except Exception as exc:
|
|
543
|
-
print(f"[pn.runtime] main-thread trampoline raised: {exc!r}")
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
# Keep a strong reference to the C-callable trampoline forever; if it
|
|
547
|
-
# gets GC'd while libdispatch holds the function pointer, we crash.
|
|
548
|
-
_main_trampoline_c = _DISPATCH_FN_TYPE(_main_trampoline_py)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def _ensure_libdispatch_loaded() -> bool:
|
|
552
|
-
"""Locate libdispatch + the main queue. Returns True on success."""
|
|
553
|
-
global _dispatch_lib, _dispatch_async_f_c, _dispatch_main_q_ptr
|
|
554
|
-
if _dispatch_lib is not None:
|
|
555
|
-
return True
|
|
556
|
-
try:
|
|
557
|
-
# PyDLL keeps the GIL held during C calls, preventing the
|
|
558
|
-
# calling thread from being parked after dispatch_async_f.
|
|
559
|
-
lib = ctypes.PyDLL("/usr/lib/system/libdispatch.dylib")
|
|
560
|
-
except OSError:
|
|
561
|
-
try:
|
|
562
|
-
lib = ctypes.PyDLL(None) # main program (libdispatch is in libSystem)
|
|
563
|
-
except OSError as exc:
|
|
564
|
-
print(f"[pn.runtime] could not load libdispatch via PyDLL: {exc!r}")
|
|
565
|
-
return False
|
|
566
|
-
try:
|
|
567
|
-
fn = lib.dispatch_async_f
|
|
568
|
-
fn.restype = None
|
|
569
|
-
fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
|
570
|
-
# The main queue is a struct global named ``_dispatch_main_q``
|
|
571
|
-
# in libdispatch. dispatch_async_f wants a pointer to it.
|
|
572
|
-
sym = ctypes.c_void_p.in_dll(lib, "_dispatch_main_q")
|
|
573
|
-
main_q_ptr = ctypes.addressof(sym)
|
|
574
|
-
except (AttributeError, ValueError) as exc:
|
|
575
|
-
print(f"[pn.runtime] libdispatch missing expected symbol: {exc!r}")
|
|
576
|
-
return False
|
|
577
|
-
_dispatch_lib = lib
|
|
578
|
-
_dispatch_async_f_c = fn
|
|
579
|
-
_dispatch_main_q_ptr = main_q_ptr
|
|
580
|
-
return True
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
_pthread_main_np_c: Optional[Any] = None
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
def _ios_is_main_thread() -> bool:
|
|
587
|
-
"""Cheap main-thread check using pthread_main_np (no GIL release)."""
|
|
588
|
-
try:
|
|
589
|
-
# pthread_main_np is in libc; calling via PyDLL keeps the GIL.
|
|
590
|
-
global _pthread_main_np_c
|
|
591
|
-
if _pthread_main_np_c is None:
|
|
592
|
-
libc = ctypes.PyDLL(ctypes.util.find_library("c") or "/usr/lib/libc.dylib")
|
|
593
|
-
f = libc.pthread_main_np
|
|
594
|
-
f.restype = ctypes.c_int
|
|
595
|
-
f.argtypes = []
|
|
596
|
-
_pthread_main_np_c = f
|
|
597
|
-
return bool(_pthread_main_np_c())
|
|
598
|
-
except Exception:
|
|
599
|
-
# Fall back to threading module (Python-side check).
|
|
600
|
-
return threading.current_thread().name == "MainThread"
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
def _ios_dispatch_async(fn: Callable[[], None]) -> None:
|
|
604
|
-
"""Enqueue ``fn`` on the iOS main dispatch queue (always queued)."""
|
|
605
|
-
global _main_next_id
|
|
606
|
-
if not _ensure_libdispatch_loaded():
|
|
607
|
-
print("[pn.runtime] libdispatch unavailable; running callback inline")
|
|
608
|
-
try:
|
|
609
|
-
fn()
|
|
610
|
-
except Exception as exc:
|
|
611
|
-
print(f"[pn.runtime] inline fallback raised: {exc!r}")
|
|
612
|
-
return
|
|
613
|
-
with _main_pending_lock:
|
|
614
|
-
_main_next_id += 1
|
|
615
|
-
key = _main_next_id
|
|
616
|
-
_main_pending[key] = fn
|
|
617
|
-
# dispatch_async_f(queue, context, work): non-blocking; just
|
|
618
|
-
# enqueues the work onto the main queue and returns.
|
|
619
|
-
assert _dispatch_async_f_c is not None
|
|
620
|
-
_dispatch_async_f_c(_dispatch_main_q_ptr, key, _main_trampoline_c)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def _ios_call_on_main(fn: Callable[[], None]) -> None:
|
|
624
|
-
"""Run ``fn`` on the iOS main thread (inline when already there)."""
|
|
625
|
-
if _ios_is_main_thread():
|
|
626
|
-
try:
|
|
627
|
-
fn()
|
|
628
|
-
except Exception as exc:
|
|
629
|
-
print(f"[pn.runtime] main-inline callback raised: {exc!r}")
|
|
630
|
-
return
|
|
631
|
-
_ios_dispatch_async(fn)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
# Cached Android main-queue plumbing. Posting a pump is a hot path (it
|
|
635
|
-
# runs for every batch of scheduled loop work), so the Runnable proxy
|
|
636
|
-
# class and the main-looper Handler are created once and reused;
|
|
637
|
-
# defining a ``dynamic_proxy`` class per call would register a new JNI
|
|
638
|
-
# class every time.
|
|
639
|
-
_android_runnable_cls: Optional[Any] = None
|
|
640
|
-
_android_main_handler: Optional[Any] = None
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def _ensure_android_main_handler() -> Any:
|
|
644
|
-
global _android_runnable_cls, _android_main_handler
|
|
645
|
-
if _android_main_handler is not None:
|
|
646
|
-
return _android_main_handler
|
|
647
|
-
from java import dynamic_proxy, jclass
|
|
648
|
-
|
|
649
|
-
Runnable = jclass("java.lang.Runnable")
|
|
650
|
-
|
|
651
|
-
class _PNMainRunnable(dynamic_proxy(Runnable)): # type: ignore[misc]
|
|
652
|
-
def __init__(self, fn: Callable[[], None]) -> None:
|
|
653
|
-
super().__init__()
|
|
654
|
-
self._fn = fn
|
|
655
|
-
|
|
656
|
-
def run(self) -> None:
|
|
657
|
-
try:
|
|
658
|
-
self._fn()
|
|
659
|
-
except Exception as exc: # pragma: no cover - last resort
|
|
660
|
-
print(f"[pn.runtime] main-thread runnable raised: {exc!r}")
|
|
661
|
-
|
|
662
|
-
Looper = jclass("android.os.Looper")
|
|
663
|
-
Handler = jclass("android.os.Handler")
|
|
664
|
-
_android_runnable_cls = _PNMainRunnable
|
|
665
|
-
_android_main_handler = Handler(Looper.getMainLooper())
|
|
666
|
-
return _android_main_handler
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def _android_post_to_main(fn: Callable[[], None]) -> None:
|
|
670
|
-
"""Enqueue ``fn`` on the Android main looper (always queued)."""
|
|
671
|
-
try:
|
|
672
|
-
handler = _ensure_android_main_handler()
|
|
673
|
-
assert _android_runnable_cls is not None
|
|
674
|
-
handler.post(_android_runnable_cls(fn))
|
|
675
|
-
except Exception as exc:
|
|
676
|
-
print(f"[pn.runtime] _android_post_to_main failed, falling back inline: {exc!r}")
|
|
677
|
-
fn()
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
def _android_call_on_main(fn: Callable[[], None]) -> None:
|
|
681
|
-
"""Run ``fn`` on the Android main thread (inline when already there)."""
|
|
682
|
-
try:
|
|
683
|
-
from java import jclass
|
|
684
|
-
|
|
685
|
-
Looper = jclass("android.os.Looper")
|
|
686
|
-
if Looper.myLooper() == Looper.getMainLooper():
|
|
687
|
-
fn()
|
|
688
|
-
return
|
|
689
|
-
except Exception:
|
|
690
|
-
pass
|
|
691
|
-
_android_post_to_main(fn)
|
|
692
|
-
|
|
693
|
-
|
|
694
526
|
__all__ = [
|
|
695
527
|
"call_on_main_thread",
|
|
696
528
|
"call_threadsafe",
|